Configuring Payment & API Keys
E-commerce apps need sensitive configuration: payment gateway keys, database credentials, third-party API tokens. Docker provides several ways to handle these securely.
The Wrong Way
Never put secrets in your Dockerfile:
# NEVER DO THIS!
FROM node:20-alpine
ENV STRIPE_SECRET_KEY=sk_live_abc123xyz
ENV DATABASE_PASSWORD=supersecret
COPY . .
CMD ["node", "server.js"]
This bakes secrets into the image. Anyone who pulls your image can extract them:
docker history your-image --no-trunc
# Shows all ENV values!
Environment Variables at Runtime
The right approach: Pass secrets when running the container.
docker run -d \
--name payment-service \
-e STRIPE_SECRET_KEY=sk_live_abc123xyz \
-e STRIPE_WEBHOOK_SECRET=whsec_xyz789 \
-e DATABASE_URL=postgresql://user:pass@db:5432/shop \
-p 3000:3000 \
shopflow/payment-service:1.0
Secrets exist only in the running container, not in the image.
Reading Environment Variables
Node.js:const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const dbConfig = {
connectionString: process.env.DATABASE_URL,
ssl: process.env.NODE_ENV === 'production'
};
// With defaults
const port = process.env.PORT || 3000;
const logLevel = process.env.LOG_LEVEL || 'info';
Python:
import os
import stripe
stripe.api_key = os.environ['STRIPE_SECRET_KEY']
db_url = os.environ.get('DATABASE_URL', 'postgresql://localhost/shop')
debug = os.environ.get('DEBUG', 'false').lower() == 'true'
E-commerce Configuration Example
Here's a complete payment service setup:
src/config.js:const requiredVars = [
'STRIPE_SECRET_KEY',
'STRIPE_WEBHOOK_SECRET',
'DATABASE_URL',
'REDIS_URL'
];
// Validate all required vars are set
for (const varName of requiredVars) {
if (!process.env[varName]) {
console.error(`Missing required environment variable: ${varName}`);
process.exit(1);
}
}
module.exports = {
stripe: {
secretKey: process.env.STRIPE_SECRET_KEY,
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
currency: process.env.STRIPE_CURRENCY || 'usd'
},
database: {
url: process.env.DATABASE_URL
},
redis: {
url: process.env.REDIS_URL
},
app: {
port: parseInt(process.env.PORT) || 3000,
env: process.env.NODE_ENV || 'development',
logLevel: process.env.LOG_LEVEL || 'info'
}
};
src/server.js:
const express = require('express');
const Stripe = require('stripe');
const config = require('./config');
const app = express();
const stripe = Stripe(config.stripe.secretKey);
app.post('/api/checkout', express.json(), async (req, res) => {
try {
const { items, customerId } = req.body;
const session = await stripe.checkout.sessions.create({
customer: customerId,
payment_method_types: ['card'],
line_items: items.map(item => ({
price_data: {
currency: config.stripe.currency,
product_data: { name: item.name },
unit_amount: Math.round(item.price * 100)
},
quantity: item.quantity
})),
mode: 'payment',
success_url: `${req.headers.origin}/order/success`,
cancel_url: `${req.headers.origin}/cart`
});
res.json({ sessionId: session.id, url: session.url });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
app.post('/webhooks/stripe',
express.raw({ type: 'application/json' }),
(req, res) => {
const sig = req.headers['stripe-signature'];
try {
const event = stripe.webhooks.constructEvent(
req.body,
sig,
config.stripe.webhookSecret
);
switch (event.type) {
case 'checkout.session.completed':
console.log('Payment successful:', event.data.object.id);
// Update order status in database
break;
case 'payment_intent.payment_failed':
console.log('Payment failed:', event.data.object.id);
break;
}
res.json({ received: true });
} catch (err) {
res.status(400).send(`Webhook Error: ${err.message}`);
}
}
);
app.listen(config.app.port, () => {
console.log(`Payment service running on port ${config.app.port}`);
console.log(`Environment: ${config.app.env}`);
});
Using .env Files
For local development, create a .env file:
# Stripe Configuration
STRIPE_SECRET_KEY=sk_test_abc123
STRIPE_WEBHOOK_SECRET=whsec_test_xyz
STRIPE_CURRENCY=usd
# Database
DATABASE_URL=postgresql://shopflow:devpass@localhost:5432/shopflow
# Redis
REDIS_URL=redis://localhost:6379
# App Settings
NODE_ENV=development
PORT=3000
LOG_LEVEL=debug
.gitignore:
.env
.env.local
.env.*.local
Run with env file:
docker run -d \
--name payment-service \
--env-file .env \
-p 3000:3000 \
shopflow/payment-service:1.0
Environment File Per Stage
Create different env files for different environments:
.env.development # Local development (test keys)
.env.staging # Staging server
.env.production # Production (real keys)
.env.development:
STRIPE_SECRET_KEY=sk_test_development123
DATABASE_URL=postgresql://dev:devpass@localhost:5432/shopflow_dev
.env.production:
STRIPE_SECRET_KEY=sk_live_production789
DATABASE_URL=postgresql://prod:${DB_PASSWORD}@prod-db.example.com:5432/shopflow
# Development
docker run --env-file .env.development ...
# Production
docker run --env-file .env.production ...
Dockerfile with ENV Defaults
Set non-sensitive defaults in Dockerfile:
FROM node:20-alpine
WORKDIR /app
# Non-sensitive defaults (can be overridden at runtime)
ENV NODE_ENV=production
ENV PORT=3000
ENV LOG_LEVEL=info
# DO NOT set sensitive values here!
# ENV STRIPE_SECRET_KEY=xxx # WRONG!
COPY package*.json ./
RUN npm ci --only=production
COPY src/ ./src/
EXPOSE 3000
CMD ["node", "src/server.js"]
Override at runtime:
docker run -d \
-e NODE_ENV=staging \
-e LOG_LEVEL=debug \
-e STRIPE_SECRET_KEY=sk_test_xxx \
shopflow/payment-service:1.0
Docker Secrets (Swarm Mode)
For production clusters, Docker Secrets are more secure:
# Create a secret
echo "sk_live_abc123xyz" | docker secret create stripe_key -
# Use in service
docker service create \
--name payment-service \
--secret stripe_key \
shopflow/payment-service:1.0
In the container, secrets appear as files:
const fs = require('fs');
// Read secret from file
const stripeKey = fs.readFileSync('/run/secrets/stripe_key', 'utf8').trim();
Inspecting Environment Variables
# See env vars of a running container
docker exec payment-service env
# Or inspect the container
docker inspect payment-service --format='{{json .Config.Env}}'
Security Checklist
- [ ] Never put secrets in Dockerfile ENV
- [ ] Never commit .env files to git
- [ ] Use --env-file for local development
- [ ] Use Docker Secrets or vault for production
- [ ] Validate required env vars on startup
- [ ] Use different keys for test/staging/production
- [ ] Rotate keys periodically
Common E-commerce Environment Variables
# Payment Gateways
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
PAYPAL_CLIENT_ID=
PAYPAL_CLIENT_SECRET=
# Database
DATABASE_URL=
DATABASE_POOL_SIZE=10
# Cache
REDIS_URL=
# Email
SENDGRID_API_KEY=
FROM_EMAIL=orders@shopflow.com
# Storage (for product images)
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
S3_BUCKET=shopflow-uploads
# Third-party Services
SHIPPO_API_KEY= # Shipping
ALGOLIA_APP_ID= # Search
ALGOLIA_API_KEY=
# App Config
NODE_ENV=production
PORT=3000
JWT_SECRET=
CORS_ORIGIN=https://shopflow.com
Key Takeaways
- Never bake secrets into images - use runtime environment variables
- Use -e flag for individual variables, --env-file for many
- Create .env files per environment (dev, staging, prod)
- Validate required variables on app startup
- Use Docker Secrets for production clusters
- Never commit .env files to version control
Next, let's orchestrate our entire e-commerce stack with Docker Compose!