- Overview
- How .env Files Work
- Complete Lifecycle
- Variable Types & Availability
- Template Setup
- Configuration Examples
- Debugging & Troubleshooting
- Best Practices
- Common Pitfalls
A .env file is a simple text file that contains environment variables used by Docker Compose to configure your containers. It allows you to:
- ✅ Separate configuration from code
- ✅ Use different values per project
- ✅ Keep sensitive data out of version control
- ✅ Make configuration explicit and readable
Problem Without .env:
# Hard-coded values everywhere
services:
flutter-dev:
container_name: ledgerlinc-dev # ← Hard to reuse
build:
args:
USER_UID: 1000 # ← Hard-coded
FLUTTER_VERSION: 3.24.0 # ← Hard-codedSolution With .env:
# Flexible, reusable configuration
services:
flutter-dev:
container_name: ${PROJECT_NAME}-dev # ← From .env
build:
args:
USER_UID: ${USER_UID} # ← From .env
FLUTTER_VERSION: ${FLUTTER_VERSION} # ← From .envBenefits:
- Change project name in one place
- Different Flutter versions per project
- Template can be copied and customized easily
Docker Compose automatically looks for a .env file:
Project Directory/
├── .env ← Docker Compose finds this automatically
├── docker-compose.yml ← Uses variables from .env
└── Dockerfile
Location Rules:
- Must be named exactly
.env(not.env.txtorenv) - Must be in the same directory as
docker-compose.yml - Is read automatically by ALL
docker-composecommands
┌─────────────────────────────────────────────────────────────┐
│ Developer runs: docker-compose up │
└─────────────────────────┬───────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Docker Compose automatically reads .env file │
│ Location: Same directory as docker-compose.yml │
└─────────────────────────┬───────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Loads all KEY=VALUE pairs into memory │
└─────────────────────────┬───────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Parses docker-compose.yml │
│ Replaces ${VARIABLE_NAME} with values from .env │
└─────────────────────────┬───────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Executes commands with resolved configuration │
└─────────────────────────────────────────────────────────────┘
Before (in docker-compose.yml):
services:
flutter-dev:
container_name: ${PROJECT_NAME}-dev
build:
args:
USER_UID: ${USER_UID}What .env contains:
PROJECT_NAME=ledgerlinc
USER_UID=1000After substitution (what Docker Compose uses):
services:
flutter-dev:
container_name: ledgerlinc-dev
build:
args:
USER_UID: 1000User: Opens Dartwingers/ledgerlinc in VS Code
Action: Clicks "Reopen in Container"
┌─────────────────────────────────────────────────────────────┐
│ Runs: start-adb-if-needed.sh │
│ When: On HOST (Windows/WSL2) │
│ Note: Does NOT use .env (runs before Docker Compose) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ VS Code executes: │
│ $ cd Dartwingers/ledgerlinc │
│ $ docker-compose -f docker-compose.yml up -d │
└─────────────────────────┬───────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Docker Compose reads: │
│ 1. ./docker-compose.yml │
│ 2. ./.env (automatically, if exists) │
└─────────────────────────┬───────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Loads .env variables: │
│ PROJECT_NAME=ledgerlinc │
│ USER_NAME=developer │
│ USER_UID=1000 │
│ USER_GID=1000 │
│ FLUTTER_VERSION=3.24.0 │
└─────────────────────────┬───────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Substitutes all ${VARIABLES} in docker-compose.yml: │
│ container_name: ledgerlinc-dev │
│ user: "1000:1000" │
│ args: USER_UID=1000, USER_GID=1000, etc. │
└─────────────────────────┬───────────────────────────────────┘
↓
┌─────┴─────┐
│ Image │
│ exists? │
└─────┬─────┘
│
┌─────────────────┼─────────────────┐
│ NO │ YES
↓ ↓
┌───────────────────────┐ ┌────────────────────────┐
│ BUILD PHASE │ │ Skip build │
└───────┬───────────────┘ └────────┬───────────────┘
│ │
↓ │
┌───────────────────────────────────────────┴─────────────────┐
│ Pass build args to Dockerfile: │
│ - USER_NAME=developer │
│ - USER_UID=1000 │
│ - USER_GID=1000 │
│ - FLUTTER_VERSION=3.24.0 │
└─────────────────────────┬───────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Dockerfile receives ARGs: │
│ │
│ ARG USER_UID=1000 # ← From .env via compose │
│ ARG FLUTTER_VERSION # ← From .env via compose │
│ │
│ RUN useradd -u ${USER_UID} ... │
│ RUN git clone --branch ${FLUTTER_VERSION} ... │
└─────────────────────────┬───────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Image built with configuration baked in │
│ ARG variables are now GONE (not in image) │
└─────────────────────────┬───────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ RUN PHASE: Create and start container │
│ - Name: ledgerlinc-dev │
│ - User: 1000:1000 │
│ - Environment: ADB_SERVER_SOCKET=tcp:shared-adb-server:5037│
│ - Network: dartnet │
│ - Volumes: .:/workspace │
└─────────────────────────┬───────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Container running, ready for VS Code to attach │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ VS Code attaches to container │
│ Runs: onCreateCommand, postStartCommand, postAttachCommand │
│ Note: .env values already applied, not re-read │
└─────────────────────────────────────────────────────────────┘
Where: Dockerfile
When: During docker build
Lifetime: Gone after build completes
ARG USER_UID=1000
ARG FLUTTER_VERSION=3.24.0
# Available here during build
RUN useradd -u ${USER_UID} developer
RUN git clone --branch ${FLUTTER_VERSION} https://github.com/flutter/flutter.git
# NOT available in running container!Use for: Installing software, creating users, setting up environment
Where: Dockerfile or docker-compose.yml
When: In running container
Lifetime: Available as long as container runs
In Dockerfile:
ENV FLUTTER_ROOT=/flutter
ENV PATH=$PATH:${FLUTTER_ROOT}/bin
# Available in running containerIn docker-compose.yml:
services:
flutter-dev:
environment:
- ADB_SERVER_SOCKET=tcp:shared-adb-server:5037
- MY_CONFIG=${MY_VALUE} # From .envUse for: Configuration that apps need at runtime
Where: docker-compose.yml
When: While parsing compose file
Lifetime: Used to configure container, not passed to container
services:
flutter-dev:
container_name: ${PROJECT_NAME}-dev # ← Configures container name
user: "${USER_UID}:${USER_GID}" # ← Configures user┌────────────────────────────────────────────────────────────────┐
│ VARIABLE LIFECYCLE │
├────────────────────────────────────────────────────────────────┤
│ │
│ .env file ●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━● │
│ (disk file) Read once Done │
│ ↓ │
│ │
│ Compose parsing ●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━● │
│ Variables ${VAR} → value Done │
│ ↓ │
│ │
│ Build Phase │
│ ARG variables ●━━━━━━━━━━● │
│ Available Gone │
│ ↓ │
│ │
│ Container Running │
│ ENV variables ●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━→ │
│ Available forever │
│ │
└────────────────────────────────────────────────────────────────┘
| Variable Type | .env File | docker-compose.yml | Dockerfile Build | Running Container |
|---|---|---|---|---|
| ARG | ✅ Defined | ✅ Passed as arg | ✅ Available | ❌ Gone |
| ENV (Dockerfile) | ✅ Via ARG | ✅ Via arg | ✅ Available | ✅ Available |
| ENV (compose) | ✅ Defined | ✅ Set directly | ❌ N/A | ✅ Available |
| Compose config | ✅ Defined | ✅ Substituted | ❌ N/A | ❌ Used for config only |
DevBench/FlutterBench/templates/flutter-devcontainer-template/
├── .devcontainer/
│ └── devcontainer.json
├── .vscode/
│ └── tasks.json
├── .env.example # Template (checked into git)
├── .gitignore # Excludes .env
├── docker-compose.yml # Uses ${VARIABLES}
├── Dockerfile # Receives ARGs
└── README.md
# ====================================
# Flutter DevContainer Configuration
# ====================================
# Copy this file to .env and customize for your project
# ====================================
# Project Configuration
# ====================================
PROJECT_NAME=myproject
# ====================================
# User Configuration
# ====================================
# These should match your WSL2 user for proper file permissions
USER_NAME=developer
USER_UID=1000
USER_GID=1000
# ====================================
# Flutter Configuration
# ====================================
FLUTTER_VERSION=3.24.0
# ====================================
# Container Resources (optional)
# ====================================
CONTAINER_MEMORY=4g
CONTAINER_CPUS=2
# ====================================
# ADB Configuration
# ====================================
ADB_SERVER_HOST=shared-adb-server
ADB_SERVER_PORT=5037version: '3.8'
services:
flutter-dev:
build:
context: .
dockerfile: Dockerfile
args:
# Build-time arguments from .env
USER_NAME: ${USER_NAME:-developer}
USER_UID: ${USER_UID:-1000}
USER_GID: ${USER_GID:-1000}
FLUTTER_VERSION: ${FLUTTER_VERSION:-3.24.0}
# Container configuration from .env
container_name: ${PROJECT_NAME:-myproject}-dev
# Run as user specified in .env
user: "${USER_UID:-1000}:${USER_GID:-1000}"
# Runtime environment variables
environment:
- ADB_SERVER_SOCKET=tcp:${ADB_SERVER_HOST:-shared-adb-server}:${ADB_SERVER_PORT:-5037}
- FLUTTER_VERSION=${FLUTTER_VERSION:-3.24.0}
# Network configuration
networks:
- dartnet
# Volume mounts
volumes:
- .:/workspace
# Persist pub cache per project
- flutter-pub-cache-${PROJECT_NAME:-myproject}:/home/${USER_NAME:-developer}/.pub-cache
# Resource limits (optional)
deploy:
resources:
limits:
memory: ${CONTAINER_MEMORY:-4g}
cpus: '${CONTAINER_CPUS:-2}'
command: sleep infinity
restart: unless-stopped
# External network (managed by infrastructure)
networks:
dartnet:
external: true
name: dartnet
# Named volume for pub cache
volumes:
flutter-pub-cache-${PROJECT_NAME:-myproject}:
name: flutter-pub-cache-${PROJECT_NAME:-myproject}FROM ubuntu:22.04
# ====================================
# Build Arguments (from .env via docker-compose)
# ====================================
ARG USER_NAME=developer
ARG USER_UID=1000
ARG USER_GID=1000
ARG FLUTTER_VERSION=3.24.0
# Prevent interactive prompts
ENV DEBIAN_FRONTEND=noninteractive
# ====================================
# System Dependencies
# ====================================
RUN apt-get update && apt-get install -y \
# Essential build tools
curl \
git \
unzip \
xz-utils \
zip \
wget \
# Flutter dependencies
libglu1-mesa \
# Java for Android development
openjdk-17-jdk \
# ADB client
android-tools-adb \
# User utilities
sudo \
# Helpful tools
vim \
nano \
&& rm -rf /var/lib/apt/lists/*
# ====================================
# Create User
# ====================================
# Create group and user with specific UID/GID for file permission matching
RUN groupadd -g ${USER_GID} ${USER_NAME} 2>/dev/null || true && \
useradd -m -u ${USER_UID} -g ${USER_GID} -s /bin/bash ${USER_NAME} 2>/dev/null || true && \
echo "${USER_NAME} ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
# ====================================
# Install Flutter (as user)
# ====================================
USER ${USER_NAME}
WORKDIR /home/${USER_NAME}
# Clone Flutter at specified version
ENV FLUTTER_ROOT=/home/${USER_NAME}/flutter
RUN git clone --depth 1 --branch ${FLUTTER_VERSION} \
https://github.com/flutter/flutter.git ${FLUTTER_ROOT}
# Add Flutter to PATH
ENV PATH=$PATH:${FLUTTER_ROOT}/bin
# Pre-download Flutter dependencies and verify installation
RUN flutter precache --android && \
flutter config --no-analytics && \
flutter doctor
# ====================================
# Setup Workspace
# ====================================
WORKDIR /workspace
CMD ["bash"]# Environment variables (contains user-specific and potentially sensitive info)
.env
# Flutter
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
build/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# Logs
*.log
# OS
.DS_Store
Thumbs.dbProject: Dartwingers/ledgerlinc
.env
PROJECT_NAME=ledgerlinc
USER_NAME=developer
USER_UID=1000
USER_GID=1000
FLUTTER_VERSION=3.24.0Result:
- Container name:
ledgerlinc-dev - User:
developer(UID 1000, GID 1000) - Flutter: version 3.24.0
- Pub cache:
flutter-pub-cache-ledgerlinc
Project: Dartwingers/lablinc
.env
PROJECT_NAME=lablinc
USER_NAME=developer
USER_UID=1000
USER_GID=1000
FLUTTER_VERSION=3.19.0 # ← Older version for compatibilityResult:
- Container name:
lablinc-dev - Flutter: version 3.19.0 (different from ledgerlinc!)
- Both projects run independently
Project: DavinciDesigner/flutter-app
.env
PROJECT_NAME=davincidesigner
USER_NAME=davinci # ← Custom username
USER_UID=1500 # ← Custom UID
USER_GID=1500 # ← Custom GID
FLUTTER_VERSION=3.24.0Result:
- Container name:
davincidesigner-dev - User:
davinci(UID 1500, GID 1500) - Files created by container owned by UID 1500
Project: Large project needing more resources
.env
PROJECT_NAME=bigproject
USER_NAME=developer
USER_UID=1000
USER_GID=1000
FLUTTER_VERSION=3.24.0
CONTAINER_MEMORY=8g # ← More memory
CONTAINER_CPUS=4 # ← More CPUsResult:
- Container limited to 8GB RAM and 4 CPUs
- Better performance for large builds
# Navigate to project
cd Dartwingers/ledgerlinc
# Show resolved configuration (with all variables substituted)
docker-compose config
# Expected output shows resolved values:
# services:
# flutter-dev:
# container_name: ledgerlinc-dev # ← ${PROJECT_NAME} replaced
# user: "1000:1000" # ← ${USER_UID}:${USER_GID} replaced
# build:
# args:
# USER_UID: 1000 # ← ${USER_UID} replaced# Create test .env
cat > .env << EOF
PROJECT_NAME=testproject
TEST_VAR=hello
EOF
# View resolved config
docker-compose config | grep container_name
# Should show: container_name: testproject-dev# Without .env (uses defaults)
mv .env .env.backup
docker-compose config | grep container_name
# Shows: container_name: myproject-dev (default)
# With .env
mv .env.backup .env
docker-compose config | grep container_name
# Shows: container_name: ledgerlinc-dev (from .env)Symptom: Container name is literally ${PROJECT_NAME}-dev
Cause: .env file not found or wrong location
Solution:
# Check location
ls -la .env
ls -la docker-compose.yml
# Both should be in same directory
# Check file name
# Must be exactly ".env", not "env.txt" or ".env.local"Symptom: Container uses default values, not .env values
Cause: Syntax error in .env file
Solution:
# Check .env syntax
cat .env
# Common mistakes:
# ❌ PROJECT_NAME = ledgerlinc # Spaces around =
# ❌ PROJECT_NAME="ledgerlinc" # Quotes (become part of value)
# ❌ PROJECT NAME=ledgerlinc # Space in key
# ✅ Correct:
# PROJECT_NAME=ledgerlincSymptom: echo $MY_VAR in container shows nothing
Cause: Variable only used for compose config, not passed to container
Solution:
# To make available in container, add to environment:
services:
flutter-dev:
environment:
- MY_VAR=${MY_VAR} # Now available at runtimeSymptom: Changed .env but container still uses old values
Cause: Docker cached the build
Solution:
# Rebuild without cache
docker-compose build --no-cache
# Or force rebuild when starting
docker-compose up --build --force-recreate# Show all environment variables Docker Compose sees
docker-compose config --environment
# Show only specific service
docker-compose config --services
# Validate compose file
docker-compose config --quiet
# No output = valid, errors = invalid
# Show what variables are set in running container
docker-compose exec flutter-dev env
# Check .env file format
cat .env | grep -v '^#' | grep -v '^$'
# Should show only KEY=VALUE lines# Checked into git (safe, no secrets)
.env.example
# Not in git (user-specific, may contain secrets)
.envIn .gitignore:
# Never commit actual .env files
.env
.env.local
.env.*.local# Good: Has fallback if .env missing
USER_UID: ${USER_UID:-1000}
# Bad: Fails if .env missing
USER_UID: ${USER_UID}Syntax: ${VARIABLE:-default}
- If
VARIABLEis set in .env: uses that value - If
VARIABLEis not set: usesdefault
# .env.example
# ====================================
# Project Configuration
# ====================================
# PROJECT_NAME: Used for container name and volume names
# Must be unique across all projects
# Example: ledgerlinc, lablinc, dartwing
PROJECT_NAME=myproject
# USER_UID: User ID inside container
# Should match your WSL2 user ID (run 'id -u' to check)
# Default: 1000
USER_UID=1000Create a setup script:
scripts/setup-project.sh
#!/bin/bash
if [ ! -f .env ]; then
echo "⚠️ .env file not found!"
echo "Creating from .env.example..."
cp .env.example .env
echo "✅ Created .env file"
echo "📝 Please edit .env and set PROJECT_NAME"
exit 1
fi
# Validate required variables
REQUIRED_VARS=("PROJECT_NAME" "USER_NAME" "USER_UID" "USER_GID")
MISSING=()
for VAR in "${REQUIRED_VARS[@]}"; do
if ! grep -q "^${VAR}=" .env; then
MISSING+=("$VAR")
fi
done
if [ ${#MISSING[@]} -gt 0 ]; then
echo "❌ Missing required variables in .env:"
printf ' - %s\n' "${MISSING[@]}"
exit 1
fi
echo "✅ .env file validated"# Good: Clear what it's for
FLUTTER_VERSION=3.24.0
ADB_SERVER_HOST=shared-adb-server
CONTAINER_MEMORY=4g
# Bad: Unclear
VERSION=3.24.0
HOST=server
MEM=4g# ====================================
# Project Configuration
# ====================================
PROJECT_NAME=myproject
# ====================================
# User Configuration
# ====================================
USER_NAME=developer
USER_UID=1000
USER_GID=1000
# ====================================
# Flutter Configuration
# ====================================
FLUTTER_VERSION=3.24.0# Always test compose config
docker-compose config
# Test build
docker-compose build
# Test run
docker-compose up -d
docker-compose exec flutter-dev flutter --version
docker-compose down# WRONG - .env in parent directory
Dartwingers/
├── .env # ← Won't be found!
└── ledgerlinc/
└── docker-compose.yml
# CORRECT - .env next to docker-compose.yml
Dartwingers/
└── ledgerlinc/
├── .env # ← Here!
└── docker-compose.yml# WRONG - Quotes become part of value
PROJECT_NAME="ledgerlinc"
# Result: container name is "ledgerlinc"-dev (with quotes!)
# CORRECT - No quotes
PROJECT_NAME=ledgerlinc# WRONG
PROJECT_NAME = ledgerlinc # Spaces cause parsing errors
# CORRECT
PROJECT_NAME=ledgerlinc # No spaces# .env is NOT copied into the container
# It's only read by Docker Compose on the HOST
# Won't work:
docker exec my-container cat .env # File doesn't exist# WRONG - Trying to use .env for app config
# File: app.py
from dotenv import load_dotenv
load_dotenv() # Won't find .env (not in container!)
# CORRECT - Use environment variables passed by compose
# File: docker-compose.yml
environment:
- MY_CONFIG=${MY_VALUE} # From .env, passed to container
# File: app.py
import os
config = os.environ.get('MY_CONFIG') # Works!# Change .env
echo "FLUTTER_VERSION=3.19.0" >> .env
# Won't take effect automatically
docker-compose up # Still uses old version!
# CORRECT - Rebuild
docker-compose up --build# NEVER commit .env - it may contain:
# - User-specific paths
# - API keys
# - Passwords
# - Machine-specific configuration
# Always in .gitignore:
.env
.env.local
.env.*.local
# Only commit:
.env.example# WRONG - Docker Compose doesn't support multiline in .env
MY_VALUE=line1
line2
line3
# CORRECT - Use quotes and \n or put in file and mount
MY_VALUE="line1\nline2\nline3"
# Or better - mount a file:
volumes:
- ./config.txt:/app/config.txt| Step | File | Action | .env Used? |
|---|---|---|---|
| 1 | .devcontainer/devcontainer.json |
initializeCommand runs | ❌ No |
| 2 | .env |
Docker Compose starts | ✅ Read automatically |
| 3 | docker-compose.yml |
Parse and substitute | ✅ Variables replaced |
| 4 | Dockerfile |
Build image (if needed) | ✅ Via build args |
| 5 | Container | Start container | ✅ Via environment vars |
| 6 | Container | Running | ❌ Values already applied |
.env.example (template in git)
↓
Copy to
↓
.env (user's local config)
↓
Read by
↓
docker-compose.yml (substitutes ${VARS})
↓
Passes args to
↓
Dockerfile (receives ARGs, builds image)
↓
Creates
↓
Container (runs with ENV vars)
# Comments start with #
KEY=value
KEY_TWO=value_two
# No spaces around =
# No quotes needed
# One variable per line
# Empty lines ignoreddocker-compose config # ✅ Uses .env
docker-compose build # ✅ Uses .env
docker-compose up # ✅ Uses .env
docker-compose down # ✅ Uses .env
docker-compose restart # ✅ Uses .env
docker-compose exec # ✅ Container has ENV vars from .env# Basic
${VARIABLE}
# With default
${VARIABLE:-default}
# Must be set (error if missing)
${VARIABLE?error message}
# Use value or empty
${VARIABLE-default}# 1. Verify .env exists
ls -la .env
# 2. Check .env syntax
cat .env | grep -v '^#' | grep -v '^$'
# 3. See what compose will use
docker-compose config
# 4. Test build
docker-compose build
# 5. Verify in running container
docker-compose up -d
docker-compose exec flutter-dev env | grep MY_VAR✅ Automatic: Docker Compose reads .env automatically from the same directory
✅ Parse Time: Variables substituted before anything else happens
✅ Build Args: Passed to Dockerfile during image build
✅ Runtime ENV: Available in running containers
✅ Per Project: Each project has its own .env for custom configuration
✅ Git Safe: .env in .gitignore, .env.example in git
When copying the template to a new project:
- ✅ Copy
.env.exampleto.env - ✅ Edit
PROJECT_NAMEin.env - ✅ Adjust other values if needed (USER_UID, FLUTTER_VERSION, etc.)
- ✅ Open in VS Code → Reopen in Container
- ✅ Docker Compose automatically uses
.env - ✅ Container built and configured correctly
That's it! The .env file makes each project independently configurable while using the same template structure.
DevBench/FlutterBench/templates/flutter-devcontainer-template/
flutter-infrastructure-architecture.md- Overall architecturevscode-tasks-snippets.md- Tasks and configuration snippetspath-pinning-verification.md- Infrastructure paths
For issues with .env files in Flutter DevContainers, check:
- This guide's troubleshooting section
- Run
docker-compose configto see resolved values - Validate
.envsyntax (no spaces, no quotes) - Ensure
.envis in same directory asdocker-compose.yml