Self-Hosting
Deploy dTax on your own Linux server in under 15 minutes using Docker Compose.
Prerequisites
- Linux server (Ubuntu 22.04+ recommended), 2+ GB RAM
- Docker Engine 24+ and Docker Compose v2
- A domain name pointed to your server’s IP
- Ports 80 and 443 open in your firewall
Quick Start
-
Clone the repository
Terminal window git clone https://github.com/dTaxLab/dtax.gitcd dtax -
Configure environment
Terminal window cp .env.production.example .envEdit
.envand set all required values:Variable Description POSTGRES_PASSWORDStrong database password JWT_SECRETRandom string, at least 32 characters ENCRYPTION_KEY64-character hex string — see note below CORS_ORIGINYour domain, e.g. https://tax.example.comAPP_URLSame domain (used in password-reset emails) NEXT_PUBLIC_API_URLhttps://tax.example.com/api(build-time, must be set beforedocker compose build) -
Start services
This starts PostgreSQL, Redis, API, Web, and nginx. The
migrateservice runs database migrations automatically.Verify everything is running:
Terminal window docker compose pscurl http://localhost/api/health
Architecture
┌──────────┐ :80/:443 ────│ nginx │ └────┬─────┘ ┌────┴─────┐ ┌─────│ routes │─────┐ │ └──────────┘ │ ┌────┴───┐ ┌─────┴────┐ │ API │ │ Web │ │ :3001 │ │ :3000 │ │ │ │ Sync │ (embedded in API process) │ Worker │ polls DB every 5s │ ├── WALLET_SYNC jobs │ └── PRICE_ENRICH jobs → auto tax calc → email └────┬───┘ ┌────┴───┐ ┌───────┐ │Postgres│ │ Redis │ └────────┘ └───────┘The Sync Worker runs inside the API process and polls PostgreSQL for pending jobs every 5 seconds. It handles two job types:
WALLET_SYNC— fetches on-chain or exchange transactionsPRICE_ENRICH— backfills missing USD prices, then automatically calculates FIFO tax reports and sends a summary email
No separate worker container is needed. Configure RESEND_API_KEY + FROM_EMAIL + APP_URL to enable tax-ready email notifications.
TLS with Let’s Encrypt
-
Update nginx config
Edit
docker/nginx/nginx.confand replaceserver_name _;with your domain:server_name tax.example.com; -
Obtain certificate
Terminal window docker compose up -d nginxdocker run --rm \-v dtax_letsencrypt:/etc/letsencrypt \-v dtax_certbot-data:/var/www/certbot \certbot/certbot certonly \--webroot -w /var/www/certbot \-d tax.example.com \--agree-tos --email you@example.com -
Enable HTTPS
Add an HTTPS server block to
docker/nginx/nginx.conf:server {listen 443 ssl http2;server_name tax.example.com;ssl_certificate /etc/letsencrypt/live/tax.example.com/fullchain.pem;ssl_certificate_key /etc/letsencrypt/live/tax.example.com/privkey.pem;include /etc/nginx/snippets/ssl-params.conf;location /api/ {proxy_pass http://api/api/;proxy_set_header Host $host;proxy_set_header X-Real-IP $remote_addr;proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;proxy_set_header X-Forwarded-Proto $scheme;proxy_buffering off;proxy_cache off;proxy_read_timeout 300s;}location / {proxy_pass http://web;proxy_set_header Host $host;proxy_set_header X-Real-IP $remote_addr;proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;proxy_set_header X-Forwarded-Proto $scheme;}}Terminal window docker compose restart nginx -
Auto-renew certificates
Terminal window echo "0 3 * * * docker run --rm \-v dtax_letsencrypt:/etc/letsencrypt \-v dtax_certbot-data:/var/www/certbot \certbot/certbot renew --quiet \&& docker compose restart nginx" | crontab -
First Admin User
- Register at
https://yourdomain.com/register - Promote your account to admin:
docker compose exec postgres psql -U dtax -c \ "UPDATE \"User\" SET role='ADMIN' WHERE email='your@email.com';"Updates
git pulldocker compose builddocker compose up -dThe migrate service runs automatically to apply new database migrations.
Backups
# Run backupchmod +x docker/scripts/backup.sh./docker/scripts/backup.sh ./backups
# Schedule daily at 2amecho "0 2 * * * /path/to/dtax/docker/scripts/backup.sh /path/to/dtax/backups" | crontab -
# Restore from backupgunzip -c backups/dtax_20260313_020000.sql.gz | \ docker compose exec -T postgres psql -U dtax dtaxOptional Services
| Service | Environment Variables | Purpose |
|---|---|---|
| Resend | RESEND_API_KEY, FROM_EMAIL | Email verification, password reset, and automatic tax-ready notifications — without this, password reset, email verification, and post-enrichment tax report emails are silently skipped |
| Stripe | STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_PRO_PRICE_ID, STRIPE_CPA_PRICE_ID | Subscription billing |
| Anthropic | ANTHROPIC_API_KEY | AI transaction classification & chat |
| Etherscan | ETHERSCAN_API_KEY | EVM blockchain indexing (Ethereum, Polygon, BSC, Arbitrum, Optimism) |
| Solscan | SOLSCAN_API_KEY | Solana blockchain indexing |
| PostHog | NEXT_PUBLIC_POSTHOG_KEY, NEXT_PUBLIC_POSTHOG_HOST | Product analytics |
| Sentry | SENTRY_DSN | Error tracking |
All optional services degrade gracefully when not configured.
Troubleshooting
Services fail to start
docker compose logs api # Check API logsdocker compose logs web # Check Web logsdocker compose logs nginx # Check nginx logsAPI crashes immediately
Check for FATAL: ENCRYPTION_KEY in the API logs — this means your ENCRYPTION_KEY is not a valid 64-character hex string. Regenerate with openssl rand -hex 32.
Database connection errors
Ensure POSTGRES_PASSWORD in .env matches the value used when the postgres volume was first created:
docker compose ps postgresdocker compose logs postgresPort conflicts
If ports 80 or 443 are already in use, edit docker-compose.yml to change the nginx port mappings.
Out of disk space
docker system prune -f # Remove unused images/containersReset everything
docker compose down -vdocker compose up -d