<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mohammed Tayeh</title>
    <description>The latest articles on DEV Community by Mohammed Tayeh (@tayeh).</description>
    <link>https://dev.to/tayeh</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F517271%2F9a209623-0c5e-491c-86f5-c55806d44acc.jpeg</url>
      <title>DEV Community: Mohammed Tayeh</title>
      <link>https://dev.to/tayeh</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tayeh"/>
    <language>en</language>
    <item>
      <title>Kubernetes Secrets That Sync Themselves — the SnapEnv Operator, Auto-Reload, and Helm</title>
      <dc:creator>Mohammed Tayeh</dc:creator>
      <pubDate>Tue, 15 Sep 2026 15:38:04 +0000</pubDate>
      <link>https://dev.to/tayeh/kubernetes-secrets-that-sync-themselves-the-snapenv-operator-auto-reload-and-helm-4jnk</link>
      <guid>https://dev.to/tayeh/kubernetes-secrets-that-sync-themselves-the-snapenv-operator-auto-reload-and-helm-4jnk</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 3 of 3. &lt;a href="https://dev.to/tayeh/from-slack-pasted-env-files-to-snapenv-a-10-minute-setup-guide-2k2j"&gt;Part 1&lt;/a&gt; covers the dashboard and CLI; &lt;a href="https://dev.to/tayeh/running-snapenv-on-a-bare-linux-server-with-systemd-31g1"&gt;part 2&lt;/a&gt; covers a bare Linux server with systemd. This one's for Kubernetes.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If your deployment story today is "someone runs &lt;code&gt;kubectl create secret&lt;/code&gt; by hand, or a value gets base64-encoded into a values file that shouldn't really be in git" — the SnapEnv Kubernetes operator replaces that with a &lt;code&gt;SnapEnvSecret&lt;/code&gt; custom resource that keeps a native &lt;code&gt;Secret&lt;/code&gt; in sync automatically, and can roll the Deployments that use it the moment something changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we're building
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The operator installed cluster-wide, watching for &lt;code&gt;SnapEnvSecret&lt;/code&gt; resources in any namespace&lt;/li&gt;
&lt;li&gt;One &lt;code&gt;SnapEnvSecret&lt;/code&gt; synced into a native &lt;code&gt;Secret&lt;/code&gt;, wired into a Deployment via &lt;code&gt;envFrom&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;That Deployment auto-restarting when the underlying variables change — no manual rollout&lt;/li&gt;
&lt;li&gt;The same pattern generalized into a drop-in block for your own Helm chart&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. Install the operator
&lt;/h2&gt;

&lt;p&gt;The operator is a single, small (32Mi request) controller — one manifest installs the CRD, RBAC, and the Deployment itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/snapenv-io/operator/main/config/deploy/install.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That creates a dedicated &lt;code&gt;snapenv-operator&lt;/code&gt; namespace, a &lt;code&gt;ClusterRole&lt;/code&gt; scoped to exactly what it needs (read &lt;code&gt;SnapEnvSecret&lt;/code&gt; CRs, read/write &lt;code&gt;Secret&lt;/code&gt;s, read/patch &lt;code&gt;Deployment&lt;/code&gt;s and &lt;code&gt;StatefulSet&lt;/code&gt;s for the restart feature, write &lt;code&gt;Event&lt;/code&gt;s), and a one-replica Deployment running &lt;code&gt;ghcr.io/snapenv-io/operator:latest&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; snapenv-operator get pods
NAME                                READY   STATUS    RESTARTS   AGE
snapenv-operator-669d6b9d54-gwcql   1/1     Running   0          16m
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. The SnapEnvSecret custom resource
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;snapenv.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SnapEnvSecret&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-web-app-dev&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-namespace&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;tokenSecret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;snapenv-token&lt;/span&gt;      &lt;span class="c1"&gt;# name of a Secret in this namespace holding the snp_live_ token&lt;/span&gt;
  &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ad12d532-0779-49fa-8045-eed069db8597&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dev&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-web-app-secret&lt;/span&gt;       &lt;span class="c1"&gt;# the native Secret the operator will create/keep in sync&lt;/span&gt;
  &lt;span class="na"&gt;syncInterval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1m&lt;/span&gt;                &lt;span class="c1"&gt;# optional — default 30m, minimum 1m&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Required&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tokenSecret&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;Name of a &lt;code&gt;Secret&lt;/code&gt; in the &lt;strong&gt;same namespace&lt;/strong&gt;, containing the token under a &lt;code&gt;token&lt;/code&gt; key (override with &lt;code&gt;tokenKey&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;project&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;The SnapEnv project UUID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;env&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;Environment name (&lt;code&gt;prod&lt;/code&gt;, &lt;code&gt;staging&lt;/code&gt;, &lt;code&gt;dev&lt;/code&gt;, …)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;target&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;The native &lt;code&gt;Secret&lt;/code&gt; name the operator manages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;apiUrl&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;Override the API base URL — must be &lt;code&gt;https://&lt;/code&gt;, enforced by the CRD schema itself&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;syncInterval&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;Poll interval absent a webhook push. Default &lt;code&gt;30m&lt;/code&gt;, clamps to a 1m minimum&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;allowEmpty&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;Off by default — a sync that returns zero variables &lt;strong&gt;won't&lt;/strong&gt; overwrite a target that previously had data, since an empty response is far more likely to be a permissions/deploy mistake upstream than an intentional wipe&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The token only ever needs &lt;strong&gt;read&lt;/strong&gt; scope, pinned to the one project and environment it's syncing — exactly the same least-privilege instinct as the &lt;a href="https://dev.to/tayeh/running-snapenv-on-a-bare-linux-server-with-systemd-31g1"&gt;systemd guide&lt;/a&gt;'s token.&lt;/p&gt;

&lt;p&gt;Applying it against a real (disposable, demo) project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;kubectl create secret generic snapenv-token &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--from-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./token.txt &lt;span class="nt"&gt;-n&lt;/span&gt; my-namespace

&lt;span class="nv"&gt;$ &lt;/span&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; snapenvsecret.yaml
snapenvsecret.snapenv.io/my-web-app-dev created

&lt;span class="nv"&gt;$ &lt;/span&gt;kubectl get snapenvsecret my-web-app-dev
NAME             PROJECT                                ENV   TARGET              READY   VARS   LAST SYNC
my-web-app-dev   ad12d532-0779-49fa-8045-eed069db8597   dev   my-web-app-secret   &lt;span class="nb"&gt;true    &lt;/span&gt;8       9s

&lt;span class="nv"&gt;$ &lt;/span&gt;kubectl describe secret my-web-app-secret
Name:         my-web-app-secret
Labels:       app.kubernetes.io/managed-by&lt;span class="o"&gt;=&lt;/span&gt;snapenv-operator
              snapenv.io/env&lt;span class="o"&gt;=&lt;/span&gt;dev
              snapenv.io/project&lt;span class="o"&gt;=&lt;/span&gt;ad12d532-0779-49fa-8045-eed069db8597
Annotations:  snapenv.io/data-hash: W/&lt;span class="s2"&gt;"d2fb7d8f49100323"&lt;/span&gt;
              snapenv.io/last-sync-time: 2026-09-15T12:22:27Z
Type:  Opaque
Data
&lt;span class="o"&gt;====&lt;/span&gt;
API_KEY:               28 bytes
DATABASE_URL:          39 bytes
LOG_LEVEL:             5 bytes
PORT:                  4 bytes
REDIS_URL:             24 bytes
SNAPENV_ENV:           3 bytes
SNAPENV_PROJECT:       36 bytes
SNAPENV_PROJECT_NAME:  10 bytes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same three synthetic &lt;code&gt;SNAPENV_*&lt;/code&gt; context variables the CLI injects on every &lt;code&gt;pull&lt;/code&gt; show up here too — useful for a pod to introspect which project/env it's actually running against without you wiring that up separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Wire it into a Deployment
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-web-app&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-namespace&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;my-web-app&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;my-web-app&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;snapenv.io/sync-secret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-web-app-secret&lt;/span&gt;   &lt;span class="c1"&gt;# see step 4&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-web-app&lt;/span&gt;
          &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/you/my-web-app:latest&lt;/span&gt;
          &lt;span class="na"&gt;envFrom&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;secretRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-web-app-secret&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing SnapEnv-specific in the container itself — it's a normal &lt;code&gt;envFrom.secretRef&lt;/code&gt;, exactly like referencing any other Kubernetes &lt;code&gt;Secret&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Auto-reload when secrets change
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;snapenv.io/sync-secret: "&amp;lt;target-secret-name&amp;gt;"&lt;/code&gt; annotation above is what makes this self-driving. It can sit on the Deployment's own metadata or on its pod template (the operator checks both, since some Helm conventions put annotations in one place or the other) — when the operator resyncs and the secret's content actually changed, it patches &lt;code&gt;kubectl.kubernetes.io/restartedAt&lt;/code&gt; onto the pod template of every Deployment/StatefulSet carrying that annotation. That's the same mechanism &lt;code&gt;kubectl rollout restart&lt;/code&gt; uses — a normal, native rolling restart, not a hard delete.&lt;/p&gt;

&lt;p&gt;Critically, &lt;strong&gt;it only restarts on an actual content change&lt;/strong&gt; — the operator stores the API's ETag on the target Secret (&lt;code&gt;snapenv.io/data-hash&lt;/code&gt;) and compares it before writing anything, so a poll that finds nothing new is a no-op (no restart, and if the API returns &lt;code&gt;304 Not Modified&lt;/code&gt;, not even a Secret write). That distinction matters: an earlier version of this exact operator had a bug where a broken ETag comparison made &lt;em&gt;every&lt;/em&gt; poll look like a change, restart-looping every linked Deployment roughly once a minute. Diffing before you act — not "sync on every poll" — is the difference between a self-healing system and a self-DDoSing one.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Verify from the dashboard
&lt;/h2&gt;

&lt;p&gt;Every sync the operator performs sends &lt;code&gt;X-SnapEnv-Client: operator&lt;/code&gt;, which the dashboard's &lt;strong&gt;Integrations&lt;/strong&gt; page uses to show a live "last synced" time per project — a quick way to confirm the operator is actually running against a given project without reaching for &lt;code&gt;kubectl&lt;/code&gt; at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. The Helm chart pattern
&lt;/h2&gt;

&lt;p&gt;If you maintain your own chart, the pattern that works well is a &lt;code&gt;snapenv&lt;/code&gt; block in &lt;code&gt;values.yaml&lt;/code&gt;, mutually exclusive with however you currently inject secrets (a plain base64 &lt;code&gt;Secret&lt;/code&gt;, Doppler, whatever):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# values.yaml&lt;/span&gt;
&lt;span class="na"&gt;snapenv&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="na"&gt;tokenSecret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;              &lt;span class="c1"&gt;# defaults to &amp;lt;app-name&amp;gt;-snapenv-token&lt;/span&gt;
    &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;              &lt;span class="c1"&gt;# snp_live_... — pass via --set-string or a values overlay, never committed&lt;/span&gt;
  &lt;span class="na"&gt;managedSecret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;              &lt;span class="c1"&gt;# defaults to &amp;lt;app-name&amp;gt;-secret&lt;/span&gt;
  &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;              &lt;span class="c1"&gt;# required when enabled&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prod&lt;/span&gt;
  &lt;span class="na"&gt;apiUrl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
  &lt;span class="na"&gt;syncInterval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30m&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# templates/snapenvsecret.yaml&lt;/span&gt;
&lt;span class="pi"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;- if .Values.snapenv.enabled&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
&lt;span class="pi"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;- if .Values.snapenv.tokenSecret.create&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Secret&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;.Values.snapenv.tokenSecret.name | default (printf "%s-snapenv-token" .Values.name)&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Opaque&lt;/span&gt;
&lt;span class="na"&gt;stringData&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;.Values.snapenv.tokenSecret.value | quote&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="pi"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;- end&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;snapenv.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SnapEnvSecret&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;.Values.name&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;&lt;span class="s"&gt;-snapenv&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;tokenSecret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;.Values.snapenv.tokenSecret.name | default (printf "%s-snapenv-token" .Values.name)&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
  &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;.Values.snapenv.project | quote&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;.Values.snapenv.env | quote&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;.Values.snapenv.managedSecret.name | default (printf "%s-secret" .Values.name)&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
  &lt;span class="pi"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;- if .Values.snapenv.syncInterval&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
  &lt;span class="na"&gt;syncInterval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;.Values.snapenv.syncInterval | quote&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
  &lt;span class="pi"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;- end&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
&lt;span class="pi"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;- end&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two helpers make this play nicely alongside whatever secret mode the chart already supports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{{/* Fail the render early if more than one secret mode is enabled at once */}}
{{- define "mychart.validateSecrets" -}}
{{- $enabled := list }}
{{- if .Values.secret.enabled }}{{- $enabled = append $enabled "secret" }}{{- end }}
{{- if .Values.snapenv.enabled }}{{- $enabled = append $enabled "snapenv" }}{{- end }}
{{- if gt (len $enabled) 1 }}
{{- fail (printf "Enable exactly one secret mode, not: %s" (join ", " $enabled)) }}
{{- end }}
{{- end }}

{{/* One name to envFrom.secretRef against, regardless of which mode is active */}}
{{- define "mychart.secretName" -}}
{{- if .Values.snapenv.enabled }}
{{- .Values.snapenv.managedSecret.name | default (printf "%s-secret" .Values.name) }}
{{- else }}
{{- printf "%s-secret" .Values.name }}
{{- end }}
{{- end }}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then every container's &lt;code&gt;envFrom&lt;/code&gt; just references &lt;code&gt;{{ include "mychart.secretName" . }}&lt;/code&gt; and doesn't care which mode produced it — flipping &lt;code&gt;snapenv.enabled: true&lt;/code&gt; in an environment's values file is the entire migration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;kubectl logs&lt;/code&gt; on the operator is flooded with &lt;code&gt;deployments.apps is forbidden&lt;/code&gt;.&lt;/strong&gt; The &lt;code&gt;ClusterRole&lt;/code&gt; is missing the &lt;code&gt;apps&lt;/code&gt; apiGroup grant on &lt;code&gt;deployments&lt;/code&gt;/&lt;code&gt;statefulsets&lt;/code&gt; — required for the auto-restart feature (step 4), not for syncing itself, so secrets will keep updating fine while this silently breaks restarts. This is easy to end up with after a redeploy: if your pipeline only updates the operator's image tag and never re-applies its RBAC manifest, a ClusterRole edit that's sitting correctly in your repo can quietly drift out of sync with what's actually applied to the cluster. Worth an explicit check after any operator upgrade:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; config/deploy/install.yaml   &lt;span class="c"&gt;# idempotent — re-applies CRD + RBAC + Deployment&lt;/span&gt;
kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; snapenv-operator logs deploy/snapenv-operator &lt;span class="nt"&gt;--tail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;20 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; forbidden
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;A &lt;code&gt;SnapEnvSecret&lt;/code&gt; shows &lt;code&gt;READY: false&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;kubectl describe snapenvsecret &amp;lt;name&amp;gt;&lt;/code&gt; — the &lt;code&gt;Last Sync Error&lt;/code&gt; field in status has the real reason, almost always a token/project/env mismatch or an expired/revoked token.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A new &lt;code&gt;status&lt;/code&gt; field you added to the CRD never shows up.&lt;/strong&gt; Kubernetes' apiserver prunes any field not declared in the CRD's OpenAPI schema — silently, no error. If you're extending this operator yourself, any new field goes in the Go type &lt;strong&gt;and&lt;/strong&gt; &lt;code&gt;config/crd/snapenv.io_snapenvsecrets.yaml&lt;/code&gt;'s &lt;code&gt;status.properties&lt;/code&gt;, or it vanishes on every write.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Restarts happen on every sync, even with nothing changed.&lt;/strong&gt; That means the ETag comparison is broken somewhere in the chain — check whether anything between the operator and the API (an ingress, a proxy) is rewriting the &lt;code&gt;ETag&lt;/code&gt;/&lt;code&gt;If-None-Match&lt;/code&gt; headers; a reverse proxy's gzip layer downgrading a strong &lt;code&gt;ETag&lt;/code&gt; to a weak &lt;code&gt;W/"..."&lt;/code&gt; one is a common culprit, and the comparison needs to tolerate that (RFC 9110 weak comparison), not do a byte-for-byte string match.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;That's all three parts of the series: &lt;a href="https://dev.to/tayeh/from-slack-pasted-env-files-to-snapenv-a-10-minute-setup-guide-2k2j"&gt;dashboard + CLI&lt;/a&gt; for how your team works day to day, &lt;a href="https://dev.to/tayeh/running-snapenv-on-a-bare-linux-server-with-systemd-31g1"&gt;systemd&lt;/a&gt; for a plain VPS, and the operator here for Kubernetes — three different delivery mechanisms, one source of truth underneath all of them. Full reference docs at &lt;a href="https://docs.snapenv.io" rel="noopener noreferrer"&gt;docs.snapenv.io&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎉 Code &lt;strong&gt;&lt;code&gt;HELLOSNAP&lt;/code&gt;&lt;/strong&gt; gets you the Pro plan free for 3 months, first 100 redemptions — redeem it from Workspace → Plan &amp;amp; Billing at &lt;a href="https://snapenv.io" rel="noopener noreferrer"&gt;snapenv.io&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>security</category>
      <category>linux</category>
    </item>
    <item>
      <title>Running SnapEnv on a Bare Linux Server with systemd</title>
      <dc:creator>Mohammed Tayeh</dc:creator>
      <pubDate>Tue, 15 Sep 2026 12:15:27 +0000</pubDate>
      <link>https://dev.to/tayeh/running-snapenv-on-a-bare-linux-server-with-systemd-31g1</link>
      <guid>https://dev.to/tayeh/running-snapenv-on-a-bare-linux-server-with-systemd-31g1</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 2 of 3 — if you haven't already, &lt;a href="https://dev.to/tayeh/from-slack-pasted-env-files-to-snapenv-a-10-minute-setup-guide-2k2j"&gt;part 1&lt;/a&gt; covers creating a project and adding your first variables from the dashboard. This guide picks up from there and assumes a project already has variables in it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Not every service runs in Kubernetes. Plenty of production workloads are still one Go binary (or Rails app, or whatever) running under &lt;code&gt;systemd&lt;/code&gt; on a plain VPS — and that's exactly the case SnapEnv's CLI was built for first: no plaintext &lt;code&gt;.env&lt;/code&gt; sitting on disk forever, no manually SSH-ing in to update a secret, and a full audit trail of every pull.&lt;/p&gt;

&lt;p&gt;This guide sets up a systemd service that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pulls its secrets from SnapEnv into memory-backed storage (&lt;code&gt;/run&lt;/code&gt;, tmpfs) — never written to persistent disk&lt;/li&gt;
&lt;li&gt;Authenticates with a token scoped to exactly one project, one environment, read-only&lt;/li&gt;
&lt;li&gt;Picks up changed secrets automatically on a timer, and only restarts when something actually changed&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why not just &lt;code&gt;scp&lt;/code&gt; a &lt;code&gt;.env&lt;/code&gt; file?
&lt;/h2&gt;

&lt;p&gt;Because that &lt;code&gt;.env&lt;/code&gt; file then sits on the server indefinitely, readable by anyone with access to the filesystem or a backup of it, with no record of who put which value there or when. SnapEnv's answer is the same one the CLI already gives you locally — &lt;code&gt;snapenv pull&lt;/code&gt; — just wired into systemd's own primitives instead of a login shell.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Install the CLI on the server
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;snapenv&lt;/code&gt; binary is a single static executable — no runtime dependencies, so it drops straight into a minimal server image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Auto-detects OS/arch, installs to ~/.local/bin/snapenv&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://get.snapenv.io/install.sh | sh

&lt;span class="c"&gt;# Or pin it to a system path directly, per architecture&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://get.snapenv.io/cli/latest/snapenv-linux-amd64 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; /usr/local/bin/snapenv &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;chmod&lt;/span&gt; +x /usr/local/bin/snapenv

&lt;span class="c"&gt;# arm64 (Graviton, Ampere, etc.)&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://get.snapenv.io/cli/latest/snapenv-linux-arm64 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; /usr/local/bin/snapenv &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;chmod&lt;/span&gt; +x /usr/local/bin/snapenv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a systemd-managed service, install to &lt;code&gt;/usr/local/bin&lt;/code&gt; — &lt;code&gt;~/.local/bin&lt;/code&gt; only works if the service runs as a user with that directory on &lt;code&gt;$PATH&lt;/code&gt;, which systemd units usually don't have.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Create a scoped, read-only token
&lt;/h2&gt;

&lt;p&gt;This is the one place least-privilege actually matters most: a token living on a server is a token that could leak in a backup, a core dump, or a misconfigured log line. Scope it as tightly as the service needs and nothing more.&lt;/p&gt;

&lt;p&gt;From &lt;strong&gt;Access Tokens → New token&lt;/strong&gt;: read-only scope, pinned to this one project, and — critically — only the &lt;code&gt;prod&lt;/code&gt; environment checked. This service has no business being able to read &lt;code&gt;dev&lt;/code&gt; or &lt;code&gt;staging&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F19pxuhyy5gr366o5v63a.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F19pxuhyy5gr366o5v63a.jpg" alt="Scoped read-only token for prod" width="800" height="469"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I picked &lt;strong&gt;No expiry&lt;/strong&gt; here deliberately — an expiring token on an unattended server is a token that silently breaks your deploy six months from now. If your org has a rotation policy, rotate on a schedule you control instead of letting an expiry date pick the moment for you.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;prod&lt;/code&gt; is a &lt;a href="https://docs.snapenv.io/guide/permissions#protected-environments" rel="noopener noreferrer"&gt;protected environment&lt;/a&gt; by default, which is why it shows a 🔒 in the dashboard — this token still works because tokens carry their own scope independent of the creator's role, but it's worth understanding &lt;em&gt;why&lt;/em&gt; prod is locked down for human accounts even while a scoped machine token can read it:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8nqecfyt4rqt1ds5ntjy.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8nqecfyt4rqt1ds5ntjy.jpg" alt="prod marked as a protected environment" width="800" height="469"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Copy the token — it's shown exactly once.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Store the token outside the unit file
&lt;/h2&gt;

&lt;p&gt;Don't put &lt;code&gt;Environment=SNAPENV_TOKEN=snp_live_...&lt;/code&gt; directly in the &lt;code&gt;.service&lt;/code&gt; file — anyone who can run &lt;code&gt;systemctl cat&lt;/code&gt; or &lt;code&gt;systemctl show&lt;/code&gt; on the unit can read it back out. Put it in its own file instead, readable only by the user the service runs as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /etc/snapenv
&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/snapenv/token.env &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
SNAPENV_TOKEN=snp_live_xxxxxxxxxxxxxxxxxxxx
SNAPENV_PROJECT=ad12d532-0779-49fa-8045-eed069db8597
SNAPENV_ENV=prod
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;sudo chown &lt;/span&gt;myapp:myapp /etc/snapenv/token.env
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;600 /etc/snapenv/token.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Swap &lt;code&gt;myapp&lt;/code&gt; for whichever system user your service runs as, and the project UUID for the one shown at the top of your own project's Variables page.)&lt;/p&gt;

&lt;p&gt;Setting all three of &lt;code&gt;SNAPENV_TOKEN&lt;/code&gt;, &lt;code&gt;SNAPENV_PROJECT&lt;/code&gt;, and &lt;code&gt;SNAPENV_ENV&lt;/code&gt; puts the CLI into &lt;strong&gt;non-interactive auto mode&lt;/strong&gt; — no &lt;code&gt;snapenv login&lt;/code&gt;, no config file, nothing written to &lt;code&gt;~/.config&lt;/code&gt;. Every &lt;code&gt;snapenv&lt;/code&gt; command in the rest of this guide just picks these up from the environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Wire it into the systemd unit
&lt;/h2&gt;

&lt;p&gt;The pattern: &lt;code&gt;ExecStartPre&lt;/code&gt; pulls fresh secrets into a &lt;code&gt;tmpfs&lt;/code&gt;-backed runtime directory, and a second &lt;code&gt;EnvironmentFile=&lt;/code&gt; loads that pulled file straight into the service's own environment — no dotenv-parsing code needed in the app itself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/systemd/system/myapp.service
&lt;/span&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;My App&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;
&lt;span class="py"&gt;Wants&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;simple&lt;/span&gt;
&lt;span class="py"&gt;User&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;myapp&lt;/span&gt;
&lt;span class="py"&gt;Group&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;myapp&lt;/span&gt;

&lt;span class="c"&gt;# tmpfs-backed /run/myapp, owned by myapp:myapp, auto-cleaned on stop —
# the pulled secrets never touch persistent disk
&lt;/span&gt;&lt;span class="py"&gt;RuntimeDirectory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;myapp&lt;/span&gt;
&lt;span class="py"&gt;RuntimeDirectoryMode&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;0700&lt;/span&gt;

&lt;span class="c"&gt;# Token + project + env for the CLI itself
&lt;/span&gt;&lt;span class="py"&gt;EnvironmentFile&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/etc/snapenv/token.env&lt;/span&gt;

&lt;span class="c"&gt;# Pull fresh secrets before every start/restart
&lt;/span&gt;&lt;span class="py"&gt;ExecStartPre&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/local/bin/snapenv pull --file /run/myapp/env&lt;/span&gt;

&lt;span class="c"&gt;# Load the pulled secrets into the service's own environment
&lt;/span&gt;&lt;span class="py"&gt;EnvironmentFile&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/run/myapp/env&lt;/span&gt;

&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/myapp/bin/server&lt;/span&gt;

&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;on-failure&lt;/span&gt;
&lt;span class="py"&gt;RestartSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;5&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; myapp.service
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status myapp.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two &lt;code&gt;EnvironmentFile=&lt;/code&gt; lines is intentional and valid — systemd merges them in order, and each is re-read fresh at start time, which is exactly what makes &lt;code&gt;ExecStartPre&lt;/code&gt; writing the second file &lt;em&gt;before&lt;/em&gt; &lt;code&gt;ExecStart&lt;/code&gt; reads it work.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Refresh secrets on a timer, restart only when something changed
&lt;/h2&gt;

&lt;p&gt;A dumb "re-pull and restart every 5 minutes" timer works, but it also bounces your service every 5 minutes even when nothing changed — noisy, and needlessly disruptive if the process is mid-request. Use &lt;code&gt;snapenv diff&lt;/code&gt; to gate the restart on there actually being a difference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# /usr/local/bin/snapenv-refresh-myapp.sh&lt;/span&gt;
&lt;span class="c"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;

&lt;span class="nv"&gt;ENV_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/run/myapp/env

&lt;span class="k"&gt;if &lt;/span&gt;snapenv diff &lt;span class="nt"&gt;--file&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ENV_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--no-metadata&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
  &lt;span class="c"&gt;# exit 0 = no differences — nothing to do&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi

&lt;/span&gt;snapenv pull &lt;span class="nt"&gt;--file&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ENV_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
systemctl restart myapp.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo chmod&lt;/span&gt; +x /usr/local/bin/snapenv-refresh-myapp.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/systemd/system/myapp-secrets-refresh.service
&lt;/span&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Check SnapEnv for changed secrets, restart myapp if needed&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;oneshot&lt;/span&gt;
&lt;span class="py"&gt;EnvironmentFile&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/etc/snapenv/token.env&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/local/bin/snapenv-refresh-myapp.sh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/systemd/system/myapp-secrets-refresh.timer
&lt;/span&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Run myapp-secrets-refresh every 5 minutes&lt;/span&gt;

&lt;span class="nn"&gt;[Timer]&lt;/span&gt;
&lt;span class="py"&gt;OnUnitActiveSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;5min&lt;/span&gt;
&lt;span class="py"&gt;OnBootSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;2min&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;timers.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; myapp-secrets-refresh.timer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check it's actually running, and only touching &lt;code&gt;myapp.service&lt;/code&gt; when it needs to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;systemctl list-timers myapp-secrets-refresh.timer
NEXT                        LEFT      LAST                         PASSED  UNIT
Thu 2026-09-17 14:05:00 UTC  4min left  Thu 2026-09-17 14:00:02 UTC  55s ago myapp-secrets-refresh.timer

&lt;span class="nv"&gt;$ &lt;/span&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; myapp-secrets-refresh.service &lt;span class="nt"&gt;--since&lt;/span&gt; &lt;span class="s2"&gt;"1 hour ago"&lt;/span&gt;
&lt;span class="nt"&gt;--&lt;/span&gt; no differences found, restart skipped &lt;span class="nt"&gt;--&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  6. Rotating the token
&lt;/h2&gt;

&lt;p&gt;Rotate from &lt;strong&gt;Access Tokens&lt;/strong&gt; → the rotate icon next to &lt;code&gt;prod · systemd (VPS)&lt;/code&gt;. Rotation issues a new token with the same name, scope, and permissions, and &lt;strong&gt;revokes the old one immediately&lt;/strong&gt; — so update &lt;code&gt;/etc/snapenv/token.env&lt;/code&gt; first, &lt;em&gt;then&lt;/em&gt; rotate, or the old token stops working before the new one is in place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; myapp &lt;span class="nb"&gt;tee&lt;/span&gt; /etc/snapenv/token.env &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
SNAPENV_TOKEN=snp_live_&amp;lt;new-token&amp;gt;
SNAPENV_PROJECT=ad12d532-0779-49fa-8045-eed069db8597
SNAPENV_ENV=prod
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart myapp-secrets-refresh.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then check the audit log — every pull shows up there, so a rotation followed by a burst of &lt;code&gt;vars.pull&lt;/code&gt; events from the right token name is your confirmation it worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;snapenv pull&lt;/code&gt; exits 1 with an auth error.&lt;/strong&gt; The token was revoked (check if someone rotated it), expired (shouldn't happen here since we picked No expiry), or &lt;code&gt;/etc/snapenv/token.env&lt;/code&gt; has the wrong project/env. Run the pull manually as the service user to see the real error: &lt;code&gt;sudo -u myapp snapenv pull --file /tmp/test.env&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;EnvironmentFile=/run/myapp/env&lt;/code&gt; — service fails with "No such file or directory".&lt;/strong&gt; &lt;code&gt;RuntimeDirectory=&lt;/code&gt; didn't get created, or &lt;code&gt;ExecStartPre&lt;/code&gt; failed silently before writing the file. Check &lt;code&gt;journalctl -u myapp.service -n 50&lt;/code&gt; for the &lt;code&gt;ExecStartPre&lt;/code&gt; failure — it blocks &lt;code&gt;ExecStart&lt;/code&gt; from running at all, which is the correct behavior (better a failed start than a service running with stale or missing secrets).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Values look wrong / stale.&lt;/strong&gt; &lt;code&gt;EnvironmentFile=&lt;/code&gt; lines don't support &lt;code&gt;export&lt;/code&gt;, quotes around values, or inline comments — if the app was previously loading a hand-written &lt;code&gt;.env&lt;/code&gt; with any of that, the raw output from &lt;code&gt;snapenv pull&lt;/code&gt; (plain &lt;code&gt;KEY=value&lt;/code&gt;, no quoting) may parse differently. Run &lt;code&gt;snapenv pull --stdout --env prod&lt;/code&gt; locally to inspect exactly what gets written.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The refresh timer runs but never restarts the service.&lt;/strong&gt; That's &lt;code&gt;snapenv diff&lt;/code&gt; correctly reporting no differences — check with &lt;code&gt;snapenv diff --env prod --file /run/myapp/env&lt;/code&gt; by hand. If you &lt;em&gt;know&lt;/em&gt; something changed and diff disagrees, confirm &lt;code&gt;/run/myapp/env&lt;/code&gt; wasn't wiped by a reboot between pulls (it's tmpfs, so a reboot clears it — &lt;code&gt;myapp.service&lt;/code&gt;'s own &lt;code&gt;ExecStartPre&lt;/code&gt; will repopulate it on next start, but the refresh timer diffs against whatever's currently there).&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;This is part 2 of 3:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://dev.to/tayeh/from-slack-pasted-env-files-to-snapenv-a-10-minute-setup-guide-2k2j"&gt;Dashboard + CLI for developers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Linux server with systemd&lt;/strong&gt; &lt;em&gt;(this guide)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kubernetes: the operator, auto-reload, and Helm&lt;/strong&gt; — coming up next: syncing variables straight into native &lt;code&gt;Secret&lt;/code&gt; objects, auto-restarting Deployments when they change (the same "diff before restart" idea from this guide, but as a Kubernetes controller), and a drop-in pattern for adding SnapEnv to your own Helm chart.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;🎉 Code &lt;strong&gt;&lt;code&gt;HELLOSNAP&lt;/code&gt;&lt;/strong&gt; gets you the Pro plan free for 3 months, first 100 redemptions — redeem it from Workspace → Plan &amp;amp; Billing at &lt;a href="https://snapenv.io" rel="noopener noreferrer"&gt;snapenv.io&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>linux</category>
      <category>devops</category>
      <category>security</category>
      <category>cli</category>
    </item>
    <item>
      <title>From Slack-Pasted .env Files to SnapEnv — A 10-Minute Setup Guide</title>
      <dc:creator>Mohammed Tayeh</dc:creator>
      <pubDate>Tue, 15 Sep 2026 11:55:19 +0000</pubDate>
      <link>https://dev.to/tayeh/from-slack-pasted-env-files-to-snapenv-a-10-minute-setup-guide-2k2j</link>
      <guid>https://dev.to/tayeh/from-slack-pasted-env-files-to-snapenv-a-10-minute-setup-guide-2k2j</guid>
      <description>&lt;p&gt;If your team's secrets management process looks like this —&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a &lt;code&gt;.env.example&lt;/code&gt; file that's permanently out of date&lt;/li&gt;
&lt;li&gt;the &lt;em&gt;real&lt;/em&gt; &lt;code&gt;.env&lt;/code&gt; living in three people's Slack DMs&lt;/li&gt;
&lt;li&gt;nobody quite sure which value is actually live in production&lt;/li&gt;
&lt;li&gt;"wait, who has the prod database password?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;— this guide walks through replacing it with &lt;a href="https://snapenv.io" rel="noopener noreferrer"&gt;SnapEnv&lt;/a&gt; in about 10 minutes: dashboard, CLI, and a scoped token for CI. No credit card, no sales call.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Part 1 of 3 — next up: running SnapEnv on a bare Linux server with systemd, then the Kubernetes operator with Helm.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎉 &lt;strong&gt;Product Hunt readers:&lt;/strong&gt; use code &lt;strong&gt;&lt;code&gt;HELLOSNAP&lt;/code&gt;&lt;/strong&gt; for the &lt;strong&gt;Pro plan free for 3 months&lt;/strong&gt; — from &lt;strong&gt;Workspace → Plan &amp;amp; Billing&lt;/strong&gt;, click &lt;em&gt;Upgrade your plan&lt;/em&gt;, then &lt;em&gt;redeem a coupon&lt;/em&gt;. No credit card needed. Limited to the first 100 redemptions, valid through Jan 1, 2027.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What we're building
&lt;/h2&gt;

&lt;p&gt;By the end of this guide you'll have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A project with &lt;code&gt;dev&lt;/code&gt;, &lt;code&gt;staging&lt;/code&gt;, and &lt;code&gt;prod&lt;/code&gt; environments&lt;/li&gt;
&lt;li&gt;Variables added two ways (one-by-one, and bulk-imported from an existing &lt;code&gt;.env&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;snapenv&lt;/code&gt; CLI installed and pulling secrets into a local &lt;code&gt;.env&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A scoped access token, ready to hand to CI&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. Create your workspace
&lt;/h2&gt;

&lt;p&gt;Sign up at &lt;a href="https://dash.snapenv.io/signup" rel="noopener noreferrer"&gt;dash.snapenv.io&lt;/a&gt; — a workspace is created automatically, and you land straight on the Projects overview:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhztftkmhzc8e29zffw7d.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhztftkmhzc8e29zffw7d.jpg" alt="SnapEnv dashboard — projects overview" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every project here tracks its own set of environments and shows you, at a glance, how many variables exist and when something last changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Create a project
&lt;/h2&gt;

&lt;p&gt;Click &lt;strong&gt;New project&lt;/strong&gt;. Give it a name, an optional description, and pick which environments it needs. &lt;code&gt;dev&lt;/code&gt;, &lt;code&gt;staging&lt;/code&gt;, and &lt;code&gt;prod&lt;/code&gt; are selected by default, and you can add more later:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flz90gurlpjdx782wajx0.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flz90gurlpjdx782wajx0.jpg" alt="Creating a new project" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every project also gets a UUID (visible at the top of its Variables page) — that's the real identifier the API, the CLI's &lt;code&gt;--project&lt;/code&gt; flag, and &lt;code&gt;SNAPENV_PROJECT&lt;/code&gt; all use, not the display name.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;prod&lt;/code&gt; is marked &lt;strong&gt;protected&lt;/strong&gt; by default — more on why that matters in the team access section below.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Add your first variables
&lt;/h2&gt;

&lt;p&gt;Open the project and you land on its &lt;strong&gt;Variables&lt;/strong&gt; page, one tab per environment. There are two ways to get variables in:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One at a time&lt;/strong&gt;, with the type explicitly set to &lt;code&gt;Plain&lt;/code&gt; or &lt;code&gt;Secret&lt;/code&gt; (secret values are masked in the UI and in every API response unless you explicitly reveal them):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj8ymgzosoje0jl219kl1.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj8ymgzosoje0jl219kl1.jpg" alt="Add variable modal" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Or bulk-imported&lt;/strong&gt; — paste an existing &lt;code&gt;.env&lt;/code&gt; file directly in, and SnapEnv parses it, auto-detects which keys look like secrets, and lets you pick which environments to add them to:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs3bk9cbfxm5dsv2kmjkj.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs3bk9cbfxm5dsv2kmjkj.jpg" alt="Paste .env to bulk-import variables" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Either way, values are encrypted (AES-256-GCM, unique key per project) before they ever touch the database — SnapEnv's own team can't read them, and neither can anyone without explicit access to that environment. Here's the result:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbcdqb6dfvypv9i7rf3k2.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbcdqb6dfvypv9i7rf3k2.jpg" alt="Populated variables table with secrets masked" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Notice the CLI hint panel in the top right of every project page — it always shows the exact &lt;code&gt;snapenv pull&lt;/code&gt; command for whichever environment tab you're looking at. That's your cue for the next step.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Install the CLI
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;snapenv&lt;/code&gt; binary is a single static executable — no runtime, no dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://get.snapenv.io/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That detects your OS/architecture and installs to &lt;code&gt;~/.local/bin/snapenv&lt;/code&gt;. It supports macOS (amd64/arm64) and Linux (amd64/arm64) out of the box.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Authenticate and pull
&lt;/h2&gt;

&lt;p&gt;Grab an access token from &lt;strong&gt;Settings → Access tokens → New token&lt;/strong&gt; (walked through in detail in step 7 below), then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;snapenv login &lt;span class="nt"&gt;--token&lt;/span&gt; snp_live_xxxxxxxxxxxxxxxxxxxx
✓ Logged &lt;span class="k"&gt;in&lt;/span&gt; — credentials saved to ~/.config/snapenv/config.json

&lt;span class="nv"&gt;$ &lt;/span&gt;snapenv pull &lt;span class="nt"&gt;--env&lt;/span&gt; dev
✓ 5 variables written to .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it — your local &lt;code&gt;.env&lt;/code&gt; is populated, &lt;code&gt;chmod 600&lt;/code&gt;, ready to load into whatever's running your app locally. Every project has its own UUID, so if you're juggling multiple services, pin the CLI to one with &lt;code&gt;--project&lt;/code&gt; (or &lt;code&gt;SNAPENV_PROJECT&lt;/code&gt;) instead of switching context every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Promote and compare environments
&lt;/h2&gt;

&lt;p&gt;Once &lt;code&gt;dev&lt;/code&gt; is in good shape, &lt;code&gt;push&lt;/code&gt; it forward or &lt;code&gt;diff&lt;/code&gt; it against what's already live:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Push your local .env up to staging (additive — nothing gets deleted remotely)&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;snapenv push &lt;span class="nt"&gt;--env&lt;/span&gt; staging
✓ 3 created, 2 updated

&lt;span class="c"&gt;# Check what would change before you push to prod&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;snapenv diff &lt;span class="nt"&gt;--env&lt;/span&gt; prod
~ DATABASE_URL   &lt;span class="o"&gt;(&lt;/span&gt;value differs&lt;span class="o"&gt;)&lt;/span&gt;
+ FEATURE_FLAG   &lt;span class="o"&gt;(&lt;/span&gt;remote only — not &lt;span class="k"&gt;in &lt;/span&gt;&lt;span class="nb"&gt;local &lt;/span&gt;file&lt;span class="o"&gt;)&lt;/span&gt;
- DEBUG_MODE     &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;local &lt;/span&gt;only — not &lt;span class="k"&gt;in &lt;/span&gt;remote&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;diff&lt;/code&gt; exits non-zero when it finds a difference, which makes it a natural CI gate — wire it into a pipeline step and it'll fail the build the moment &lt;code&gt;prod&lt;/code&gt; config drifts from what's in source control (or vice versa).&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Invite your team — permissions that default to safe
&lt;/h2&gt;

&lt;p&gt;SnapEnv enforces three layers of access control, and the defaults are deliberately conservative: a &lt;strong&gt;Developer&lt;/strong&gt; role gets &lt;code&gt;write&lt;/code&gt; on unprotected environments like &lt;code&gt;dev&lt;/code&gt;/&lt;code&gt;staging&lt;/code&gt;, but &lt;strong&gt;no default access to &lt;code&gt;prod&lt;/code&gt;&lt;/strong&gt; at all. An Admin or Owner has to explicitly grant a developer &lt;code&gt;read&lt;/code&gt; or &lt;code&gt;write&lt;/code&gt; on a protected environment — it's opt-in, not opt-out.&lt;/p&gt;

&lt;p&gt;Invite teammates from &lt;strong&gt;Workspace → Team → Invite&lt;/strong&gt;, then fine-tune each person's per-environment access from the project's &lt;strong&gt;Team &amp;amp; Access&lt;/strong&gt; tab. Every invite, role change, and access grant is written to the audit log automatically — no separate setup required.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Create a scoped token for CI
&lt;/h2&gt;

&lt;p&gt;Humans authenticate with GitHub OAuth or email; machines (CI, the Kubernetes operator, cron jobs) use &lt;code&gt;snp_live_&lt;/code&gt; access tokens instead. Create one from &lt;strong&gt;Access Tokens → New token&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsdgggphbr3ganhlyslrd.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsdgggphbr3ganhlyslrd.jpg" alt="Access tokens page" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnmh4fp05g4j66cr1htxo.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnmh4fp05g4j66cr1htxo.jpg" alt="New access token modal" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three things worth setting deliberately here, every time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scope&lt;/strong&gt; — &lt;code&gt;Read-only&lt;/code&gt; if this token only ever needs to &lt;code&gt;pull&lt;/code&gt;. Save &lt;code&gt;Read &amp;amp; write&lt;/code&gt; for anything that also &lt;code&gt;push&lt;/code&gt;es.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Project access&lt;/strong&gt; — pin it to one project unless you genuinely need workspace-wide access.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment access&lt;/strong&gt; — a CI token for staging deploys has no business being able to read &lt;code&gt;prod&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The plaintext token is shown exactly once — copy it straight into your CI provider's secret store (&lt;code&gt;GITHUB_TOKEN&lt;/code&gt;/repo secrets, etc.) or a Kubernetes &lt;code&gt;Secret&lt;/code&gt;. If you ever lose it, rotate it; the old one is revoked the instant you do.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/deploy.yml&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pull secrets&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;SNAPENV_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SNAPENV_TOKEN }}&lt;/span&gt;
    &lt;span class="na"&gt;SNAPENV_PROJECT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ vars.SNAPENV_PROJECT }}&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;snapenv pull --env prod&lt;/span&gt;
  &lt;span class="c1"&gt;# GITHUB_ACTIONS=true is already set by the runner, so every variable&lt;/span&gt;
  &lt;span class="c1"&gt;# is also appended to $GITHUB_ENV automatically — no extra wiring needed.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;That's the core loop: dashboard for humans, CLI for machines, one source of truth for both. Full reference docs live at &lt;a href="https://docs.snapenv.io" rel="noopener noreferrer"&gt;docs.snapenv.io&lt;/a&gt; if you want to go deeper on any step above.&lt;/p&gt;

&lt;p&gt;This is part 1 of a 3-part series on running SnapEnv in production:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Dashboard + CLI for developers&lt;/strong&gt; &lt;em&gt;(this guide)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Running SnapEnv on a bare Linux server with systemd&lt;/strong&gt; — coming up next: pulling secrets into a scoped &lt;code&gt;EnvironmentFile&lt;/code&gt; and wiring a timer to pick up changes automatically, with no plaintext token in the unit file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kubernetes: the operator, auto-reload, and Helm&lt;/strong&gt; — syncing variables straight into native &lt;code&gt;Secret&lt;/code&gt; objects, auto-restarting Deployments when they change, and a drop-in pattern for adding SnapEnv as a secrets source in your own Helm chart.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;🎉 One more reminder: code &lt;strong&gt;&lt;code&gt;HELLOSNAP&lt;/code&gt;&lt;/strong&gt; gets you the Pro plan free for 3 months, first 100 redemptions — &lt;a href="https://snapenv.io" rel="noopener noreferrer"&gt;snapenv.io&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>webdev</category>
      <category>devops</category>
      <category>security</category>
    </item>
  </channel>
</rss>
