Dockyard is a CLI tool that manages PaaS infrastructure from a config file. You define your projects, applications, databases, and domains in dac.config.ts, and the tool creates, updates, or deletes resources on your provider to match. It currently supports Dokploy and Railway. The provider interface is generic, so other platforms can be added.
Your project can be written in any language. The dac.config.ts file is just a config that sits in your repo.
Requires Bun.
bun add -g dockyard-cliThat's it. You now have the dac command available everywhere.
If you don't want a global install, you can use bunx to run it without installing:
bunx dockyard-cli init
bunx dockyard-cli plan
bunx dockyard-cli applyOr build from source:
git clone https://github.com/Nu11ified/Dockyard.git
cd Dockyard && bun install && bun run build
cp dist/dac ~/.local/bin/1. Create a config file in your project:
dac initThis creates dac.config.ts in the current directory. It works in any project: Go, Python, Rust, a static site, anything.
2. Set your provider credentials:
Dokploy:
export DOKPLOY_URL="https://your-dokploy-instance.com"
export DOKPLOY_API_KEY="your-api-key"Get your API key from the Dokploy dashboard under Settings > API.
Railway:
export RAILWAY_TOKEN="your-railway-api-token"Get your token from the Railway dashboard.
3. Edit dac.config.ts:
export default {
providers: {
dokploy: {
type: "dokploy",
url: process.env.DOKPLOY_URL!,
apiKey: process.env.DOKPLOY_API_KEY!,
},
},
project: {
name: "my-app",
},
environments: {
production: {
provider: "dokploy",
applications: [{
name: "api",
source: { type: "github", repo: "myorg/api", branch: "main", owner: "myorg" },
build: { type: "dockerfile", context: "." },
domains: [{ host: "api.example.com", https: true }],
env: {
NODE_ENV: "production",
DATABASE_URL: "${{db.postgres.connectionString}}",
},
ports: [{ container: 3000 }],
}],
databases: [
{ name: "postgres", type: "postgres", version: "16" },
],
},
},
};No imports needed. The CLI validates the config at runtime.
To use Railway instead, swap the provider:
providers: {
railway: {
type: "railway",
token: process.env.RAILWAY_TOKEN!,
teamId: "optional-workspace-id", // optional
},
},The rest of the config (applications, databases, domains) stays the same. See Platform differences for what each provider supports.
4. Preview and apply:
dac plan # show what would change
dac apply # apply the changes5. Deploy:
dac deploy # deploy all applications
dac deploy api # deploy a specific appIf you want autocomplete and type checking in your config, add dockyard-cli as a dev dependency in a JS/TS project:
bun add -d dockyard-cliThen add a type annotation to your config:
import type { DacConfig } from "dockyard-cli";
export default {
// full autocomplete here
} satisfies DacConfig;This is optional. The config works without it.
| Command | Description |
|---|---|
dac init |
Create a dac.config.ts template |
dac plan |
Show planned changes without applying |
dac apply |
Apply changes (prompts for confirmation) |
dac apply --auto-approve |
Apply without confirmation |
dac deploy [app] |
Trigger deployment for one or all apps |
dac destroy |
Delete all managed resources |
dac status |
List managed resources and their IDs |
dac rollback <app> |
Rollback an application |
dokploy— Self-hosted Dokploy instance. RequiresurlandapiKey.railway— Railway.com. Requirestoken. OptionalteamIdfor workspace scoping.
github- repo, branch, owner, buildPath, watchPaths, triggerTypegitlab- repo, branch, owner, buildPath, projectId, pathNamespacebitbucket- repo, branch, owner, repositorySlug, buildPathgitea- repo, branch, owner, buildPathgit- url, branch, buildPath, sshKeyIddocker- image, registryUrl, username, password
dockerfile- dockerfile, context, buildStagenixpacks- no extra optionsbuildpacks- versionstatic- publishDirectory, isSparailpack- version
postgres, mysql, mariadb, redis, mongo
Each takes name, type, and an optional version. Credentials are auto-generated if not specified.
Use ${{db.<name>.<field>}} in environment variables to reference database connection details. These are resolved after databases are created.
env: {
DATABASE_URL: "${{db.postgres.connectionString}}",
REDIS_URL: "${{db.cache.connectionString}}",
}Applications can also include: domains, ports, mounts, redirects, security, resources (CPU/memory), replicas.
Top-level config also supports certificates and registries arrays.
Not all features are available on every provider:
| Feature | Dokploy | Railway |
|---|---|---|
| Applications | Yes | Yes |
| Databases | Yes | Yes (auto-created as services) |
| Compose | Yes | No |
| Domains | Yes | Yes (SSL automatic) |
| Mounts/Volumes | Yes | Yes |
| Redirects | Yes | No |
| Basic auth | Yes | No |
| Manual certificates | Yes | No (SSL automatic) |
Using an unsupported feature with a provider that doesn't support it will produce an error during dac plan.
Since dac.config.ts is just TypeScript, it can read process.env directly. Any environment variable you set in your GitHub Actions workflow is available in your config. This means you can store secrets (API keys, database passwords, tokens) in GitHub and have them flow into your deployments without hardcoding anything.
Create .github/workflows/deploy.yml:
name: Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- name: Plan
if: github.event_name == 'pull_request'
run: bunx dockyard-cli plan
env:
DOKPLOY_URL: ${{ secrets.DOKPLOY_URL }}
DOKPLOY_API_KEY: ${{ secrets.DOKPLOY_API_KEY }}
STRIPE_KEY: ${{ secrets.STRIPE_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
- name: Apply
if: github.event_name == 'push'
run: bunx dockyard-cli apply --auto-approve
env:
DOKPLOY_URL: ${{ secrets.DOKPLOY_URL }}
DOKPLOY_API_KEY: ${{ secrets.DOKPLOY_API_KEY }}
STRIPE_KEY: ${{ secrets.STRIPE_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
- name: Deploy
if: github.event_name == 'push'
run: bunx dockyard-cli deploy
env:
DOKPLOY_URL: ${{ secrets.DOKPLOY_URL }}
DOKPLOY_API_KEY: ${{ secrets.DOKPLOY_API_KEY }}Then in your config, reference them:
applications: [{
name: "api",
// ...
env: {
STRIPE_KEY: process.env.STRIPE_KEY!,
OPENAI_API_KEY: process.env.OPENAI_API_KEY!,
},
}],Add your secrets in your GitHub repo under Settings > Secrets and variables > Actions.
GitHub Actions has an environments feature that lets you define separate sets of secrets per environment. Each environment gets its own secrets, so STRIPE_KEY in staging can be a test key while production uses the live key.
name: Deploy
on:
push:
branches: [main, develop]
jobs:
staging:
if: github.ref == 'refs/heads/develop'
runs-on: ubuntu-latest
environment: staging
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- name: Apply staging
run: bunx dockyard-cli apply --auto-approve
env:
DOKPLOY_URL: ${{ secrets.DOKPLOY_URL }}
DOKPLOY_API_KEY: ${{ secrets.DOKPLOY_API_KEY }}
STRIPE_KEY: ${{ secrets.STRIPE_KEY }}
DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }}
APP_ENV: staging
- name: Deploy staging
run: bunx dockyard-cli deploy
env:
DOKPLOY_URL: ${{ secrets.DOKPLOY_URL }}
DOKPLOY_API_KEY: ${{ secrets.DOKPLOY_API_KEY }}
production:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- name: Apply production
run: bunx dockyard-cli apply --auto-approve
env:
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
STRIPE_KEY: ${{ secrets.STRIPE_KEY }}
DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }}
APP_ENV: production
- name: Deploy production
run: bunx dockyard-cli deploy
env:
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}Your config can then branch on APP_ENV or use different providers per environment. For example, Dokploy for staging and Railway for production:
export default {
providers: {
dokploy: {
type: "dokploy",
url: process.env.DOKPLOY_URL!,
apiKey: process.env.DOKPLOY_API_KEY!,
},
railway: {
type: "railway",
token: process.env.RAILWAY_TOKEN!,
},
},
project: { name: "my-app" },
environments: {
staging: {
provider: "dokploy",
applications: [{
name: "api",
source: { type: "github", repo: "myorg/api", branch: "develop", owner: "myorg" },
build: { type: "dockerfile" },
env: {
NODE_ENV: "staging",
STRIPE_KEY: process.env.STRIPE_KEY!,
},
}],
databases: [
{ name: "postgres", type: "postgres", version: "16" },
],
},
production: {
provider: "railway",
applications: [{
name: "api",
source: { type: "github", repo: "myorg/api", branch: "main", owner: "myorg" },
build: { type: "dockerfile" },
env: {
NODE_ENV: "production",
STRIPE_KEY: process.env.STRIPE_KEY!,
},
}],
databases: [
{ name: "postgres", type: "postgres", version: "16" },
],
},
},
};The same database and application config works on both providers. Dockyard handles the platform-specific translation.
To set this up:
- Go to your GitHub repo > Settings > Environments
- Create
stagingandproductionenvironments - Add secrets to each (same key names, different values)
- Push to
developto deploy staging, push tomainto deploy production
The same config file handles both. The secrets are different per environment because GitHub injects the right set based on the environment: field in the workflow.
The .dac/state.json file should be committed to your repo so CI knows what resources are already managed.
Dockyard keeps a state file at .dac/state.json that maps config resource names to remote IDs. On dac plan or dac apply, it diffs your config against this state and produces creates, updates, and deletes. Resources are applied in dependency order: projects, then environments, then databases, then applications.
Commit the state file to your repo. This is how Dockyard tracks what it manages.
git clone https://github.com/Nu11ified/Dockyard.git
cd Dockyard
bun install
bun test # run tests
bun run dev # run CLI without compiling
bun run build # compile to ./dist/dacMIT