DeepSeek Harness on AWS

August 20, 2026 · View on GitHub

A production-oriented deployment and integration layer for running DeepSeek Harness on AWS with Terraform, Docker Compose, persistent EBS storage, AWS Systems Manager, Secrets Manager, and an optional Discord adapter.

Compute is disposable; DSH state is persistent.

The EC2 instance can be destroyed and recreated while DeepSeek Harness state, Discord conversation history, and shared workspace data remain on a separate persistent EBS volume.

What You Get

  • Terraform-managed AWS infrastructure
  • Ubuntu 24.04 EC2 deployment
  • DeepSeek Harness pinned to 0.1.0-rc.8
  • Docker Compose runtime
  • Persistent encrypted EBS storage
  • AWS Systems Manager Session Manager access instead of inbound SSH
  • Web UI access through SSM port forwarding
  • DeepSeek API key injection from AWS Secrets Manager
  • Optional Discord bot integration
  • Discord conversation resume across process, container, and EC2 replacement
  • Separate Web and Discord session ownership
  • Shared persistent workspace where intentional
  • Disposable compute and durable data lifecycles

Architecture

AWS Secrets Manager
├── deepseek-harness/deepseek-api-key
└── deepseek-harness/discord-bot-token

             │ IAM GetSecretValue


Persistent layer — terraform/persistent/
├── encrypted EBS data volume
├── DeepSeek API key secret
└── Discord bot token secret metadata

             │ remote-state outputs


Compute layer — terraform/
├── VPC / public subnet / routing
├── security group
├── IAM role / instance profile
├── SSM access
├── EC2 instance
└── persistent EBS attachment



cloud-init
├── installs Docker / Compose / AWS CLI
├── clones this repository
├── finds and mounts persistent EBS
├── fetches runtime secrets
├── builds both containers
└── starts Docker Compose



EC2

├── DSH Web runtime
│   ├── localhost:3080
│   ├── /root/.dsh
│   └── /workspace

└── Discord adapter runtime
    ├── discord.js
    ├── official DSH SDK client
    ├── resume-aware JSON-RPC runtime
    ├── /data/sessions
    └── /workspace



Persistent EBS
/mnt/dsh-data/
├── dsh/
├── workspace/
└── discord-sessions/

The Web UI is not exposed directly to the Internet. Access is provided through AWS Systems Manager port forwarding.

Discord does not require an inbound port. The adapter connects outbound to Discord.

Persistence Model

The deployment intentionally separates session ownership:

Web runtime
└── owns Web sessions

Discord runtime
└── owns discord-<channelId> sessions

The current v1 persistence backend is JSONL.

A critical rule is:

one live DSH session
→ one authoritative runtime owner

Do not allow the Web and Discord runtimes to live-own the same session simultaneously.

The runtimes may share the persistent workspace:

/mnt/dsh-data/workspace

but session persistence is kept separate:

Web:
  /mnt/dsh-data/dsh

Discord:
  /mnt/dsh-data/discord-sessions

Prerequisites

You need:

  • AWS account
  • AWS CLI
  • Terraform
  • Git
  • Docker for local testing
  • configured AWS credentials
  • DeepSeek API key
  • optional Discord application / bot token

Verify:

aws --version
terraform version
git --version
aws sts get-caller-identity

If your AWS login session has expired, authenticate again before running Terraform.

Quick Start

1. Clone the repository

git clone https://github.com/xinyuanma/deepseek-harness-aws.git
cd deepseek-harness-aws

2. Deploy the persistent layer

cd terraform/persistent

terraform init
terraform fmt
terraform validate
terraform plan
terraform apply

Review the plan carefully before approving it.

By default, the persistent layer creates:

  • a 20 GiB encrypted gp3 EBS volume
  • a DeepSeek API key Secrets Manager secret
  • a Discord bot token Secrets Manager secret

Default deployment location:

AWS region:        eu-north-1
Availability Zone: eu-north-1b

The persistent EBS volume and EC2 instance must use the same Availability Zone. EBS volumes cannot attach across Availability Zones.

3. Store the DeepSeek API key

Terraform creates the secret container but does not manage the secret value.

Write the API key directly through AWS Secrets Manager:

aws secretsmanager put-secret-value \
  --secret-id deepseek-harness/deepseek-api-key \
  --secret-string 'YOUR_DEEPSEEK_API_KEY'

Do not put the key in committed Terraform files, .tfvars, Dockerfiles, or Git history.

4. Optional: configure Discord

Create a Discord application and bot, then write the bot token directly into Secrets Manager:

aws secretsmanager put-secret-value \
  --secret-id deepseek-harness/discord-bot-token \
  --secret-string 'YOUR_DISCORD_BOT_TOKEN'

The token value is intentionally not managed by Terraform so it does not need to be stored in Terraform state.

If you do not want Discord integration, you can leave the Discord secret without a value. The Web runtime remains usable independently.

5. Deploy the compute layer

cd ..

terraform init
terraform fmt
terraform validate
terraform plan
terraform apply

The default EC2 instance type is:

t3.small

Terraform creates the disposable compute layer and attaches the persistent EBS volume.

Cloud-init then:

  1. installs Docker and AWS CLI
  2. clones this repository
  3. detects and mounts the persistent EBS volume
  4. creates the persistent directories
  5. fetches Secrets Manager values
  6. builds the Web and Discord images
  7. starts Docker Compose

6. Wait for bootstrap

Get the SSM shell command:

terraform output -raw ssm_shell_command

Run it.

Inside EC2:

sudo cloud-init status --wait
sudo docker ps

Successful bootstrap should end with:

status: done

Expected containers:

deepseek-harness
deepseek-harness-discord

If Discord is intentionally not configured, inspect its logs if it is restarting because of missing authentication.

7. Open the Web UI

From your local machine:

terraform output -raw web_ui_forward_command

Run the command it prints, then open:

http://localhost:3080

The Web UI is accessed through SSM port forwarding.

No public inbound Web UI port is required.

Discord Integration

The Discord adapter uses:

Discord

discord.js adapter

official DeepSeek Harness SDK client

JSON-RPC runtime

DSH agent/session runtime

Current session mapping:

Discord channel
→ one DSH session

Session IDs are deterministic:

discord-<channelId>

This provides:

  • same-channel conversation continuity
  • channel isolation
  • restart resume
  • serialization within a single channel
  • concurrency across different channels

Discord UX behavior

Implemented behavior includes:

  • long-response splitting below Discord message limits
  • (1/N) pagination for multi-part responses
  • 👀 while processing
  • on success
  • on failure
  • refreshed typing indicator during long runs
  • empty and whitespace-only input handling
  • attachment-only friendly response
  • text + attachment currently processes text only
  • DSH runtime error extraction from the event stream
  • empty final-response guard
  • graceful SIGINT / SIGTERM shutdown

Full attachment and vision support are intentionally not implemented yet.

Local Discord Development

The production Discord adapter lives in:

integrations/discord/

Install:

cd integrations/discord
npm ci

Run tests:

npm test
npm run check

Start locally:

npm start

Expected environment variables:

DEEPSEEK_API_KEY
DISCORD_BOT_TOKEN

Optional:

DSH_MODEL
DSH_SESSION_ROOT
DSH_CWD

Local Docker Test

Build the Discord adapter:

cd integrations/discord
docker build -t dsh-discord:local .

Example local volumes:

docker volume create dsh-discord-sessions
docker volume create dsh-discord-workspace

Run:

docker run --rm \
  --name dsh-discord \
  --env-file .env \
  -v dsh-discord-sessions:/data/sessions \
  -v dsh-discord-workspace:/workspace \
  dsh-discord:local

The container runs Node directly as PID 1 so SIGINT / SIGTERM reach the adapter and trigger graceful shutdown.

Docker Compose

The production Compose stack is defined in:

app/docker-compose.yml

It contains:

harness
discord

The data root defaults to:

/mnt/dsh-data

and can be overridden locally with:

DSH_DATA_ROOT

For example:

export DSH_DATA_ROOT="$PWD/.local-data"
docker compose -f app/docker-compose.yml up --build

Persistent Data

AWS bind mounts:

/mnt/dsh-data/dsh
→ /root/.dsh

/mnt/dsh-data/workspace
→ /workspace

/mnt/dsh-data/discord-sessions
→ /data/sessions

The persistence lifecycle has been tested end to end across:

  • graceful process restart
  • clean npm reinstall
  • Docker container deletion and recreation
  • Docker Compose down/up
  • EC2 destruction and replacement
  • EBS reattachment
  • Discord session resume
  • older Discord conversation history recovery
  • Web UI regression checks after replacement

Destroy and Recreate Compute

To destroy the disposable compute layer while preserving EBS data:

cd terraform

terraform plan -destroy
terraform destroy

This removes resources managed by the compute Terraform layer but does not destroy resources managed by:

terraform/persistent/

Later recreate the compute layer:

terraform apply

The new EC2 instance will attach the existing EBS volume.

Web state and Discord session history stored on the persistent volume remain available.

Permanently Delete Persistent Resources

WARNING: destructive operation.

Destroy compute first:

cd terraform
terraform destroy

Then, only if you intentionally want to delete persistent user data:

cd persistent
terraform plan -destroy
terraform destroy

Destroying the persistent layer may permanently remove conversation history, settings, Discord sessions, and workspace files.

Useful Commands

Terraform outputs

terraform output
terraform output -raw instance_id
terraform output -raw ssm_shell_command
terraform output -raw web_ui_forward_command

EC2 bootstrap

sudo cloud-init status --long
sudo cloud-init status --wait
sudo tail -n 100 /var/log/cloud-init-output.log

Docker

sudo docker ps
sudo docker logs --tail 100 deepseek-harness
sudo docker logs --tail 100 deepseek-harness-discord

Storage

lsblk -o NAME,SIZE,FSTYPE,MOUNTPOINTS,SERIAL
df -h /mnt/dsh-data
sudo find /mnt/dsh-data -maxdepth 2 -type d | sort
sudo du -sh /mnt/dsh-data/*

Verify container mounts

sudo docker inspect deepseek-harness \
  --format '{{range .Mounts}}{{println .Source "->" .Destination}}{{end}}'
sudo docker inspect deepseek-harness-discord \
  --format '{{range .Mounts}}{{println .Source "->" .Destination}}{{end}}'

SDK Smoke Test

The sdk-smoke/ directory is intentionally separate from the Discord adapter.

Its purpose is to validate the official DeepSeek Harness SDK/runtime independently of Discord-specific compatibility code.

Run:

cd sdk-smoke
npm ci
npm run smoke

Expected response:

DSH SDK WORKS

Security Model

This deployment intentionally minimizes inbound exposure.

Web UI

The Harness Web UI is not exposed publicly.

Access path:

local browser

AWS SSM port forwarding

127.0.0.1:3080 on EC2

DSH Web

SSH

No inbound SSH access is required for normal administration.

Use AWS Systems Manager Session Manager.

Discord

The Discord adapter requires outbound network access but no inbound public port.

Secrets

The EC2 IAM role is granted secretsmanager:GetSecretValue only for the configured DeepSeek and Discord secret ARNs.

Secret values are fetched at runtime.

Do not commit:

.env
DISCORD_BOT_TOKEN
DEEPSEEK_API_KEY
.sessions/
node_modules/
*.tfstate
*.tfstate.*

Rotate credentials immediately if they are accidentally exposed.

Known Limitations

DSH rc.8 resume compatibility shim

Discord restart resume currently uses a local compatibility package:

integrations/discord/resume-jsonrpc-server/

It subclasses the official compiled HarnessSdkJsonRpcServer and overrides session creation so an existing persisted session is resumed before a new session is created.

This shim is intentionally coupled to DeepSeek Harness 0.1.0-rc.8.

The relevant DSH dependencies are therefore pinned to exact rc.8 versions.

When an upstream DeepSeek Harness release provides native resume behavior through the public SDK/runtime seam, this shim should be deleted rather than preserved indefinitely.

JSONL session ownership

The current v1 persistence backend is JSONL.

Do not run multiple live writers against the same session.

Discord attachments

Attachment and vision processing are not implemented yet.

Attachment-only messages receive a short unsupported-message response.

Messages containing both text and attachments currently process the text only.

Troubleshooting

Terraform cannot find AWS credentials

Verify:

aws sts get-caller-identity

Reauthenticate if necessary.

If you use an AWS CLI profile:

export AWS_PROFILE=YOUR_PROFILE

Then retry:

terraform plan

SSM does not connect immediately

The instance may still be booting or registering with Systems Manager.

Check:

terraform output -raw instance_id

Then retry the SSM session.

docker ps is empty after terraform apply

Terraform can finish before cloud-init finishes.

Inside EC2:

sudo cloud-init status --wait
sudo docker ps

If bootstrap failed:

sudo tail -n 200 /var/log/cloud-init-output.log

Discord bot does not start

Check:

sudo docker logs --tail 100 deepseek-harness-discord

Common causes include:

  • missing Discord token
  • invalid or rotated Discord token
  • missing DeepSeek API key
  • dependency/runtime startup error

Verify secret presence without printing secret values.

EBS does not attach

Ensure both persistent and compute layers use the same Availability Zone.

EBS volumes cannot attach across Availability Zones.

Discord history does not resume

Check that:

/mnt/dsh-data/discord-sessions

is mounted into:

/data/sessions

and that the same Discord channel is being used.

Also verify that only one live Discord runtime owns the session.

Repository Structure

deepseek-harness-aws/
├── app/
│   ├── Dockerfile
│   └── docker-compose.yml

├── bootstrap/
│   └── cloud-init.yaml.tftpl

├── integrations/
│   └── discord/
│       ├── Dockerfile
│       ├── .dockerignore
│       ├── package.json
│       ├── package-lock.json
│       ├── discord-dsh.mjs
│       ├── discord-input.mjs
│       ├── discord-input.test.mjs
│       ├── discord-output.mjs
│       ├── discord-reactions.mjs
│       ├── dsh-result.mjs
│       ├── session-queue.mjs
│       ├── session-queue.test.mjs
│       ├── typing-indicator.mjs
│       ├── minimal.cordis.yml
│       └── resume-jsonrpc-server/
│           ├── package.json
│           └── index.mjs

├── sdk-smoke/
│   ├── package.json
│   ├── package-lock.json
│   ├── minimal.cordis.yml
│   └── smoke.mjs

├── terraform/
│   ├── persistent/
│   │   ├── providers.tf
│   │   ├── variables.tf
│   │   ├── storage.tf
│   │   ├── secrets.tf
│   │   ├── outputs.tf
│   │   └── terraform.tfvars.example
│   ├── compute.tf
│   ├── iam.tf
│   ├── network.tf
│   ├── outputs.tf
│   ├── persistent-data.tf
│   ├── providers.tf
│   ├── variables.tf
│   └── terraform.tfvars.example

├── .gitignore
├── LICENSE
└── README.md

Project Status

The following flows have been validated end to end:

DSH Web deployment                         ✅
SSM shell access                           ✅
SSM Web port forwarding                    ✅
DeepSeek inference                         ✅
Persistent EBS mount                       ✅
Web persistence across EC2 replacement     ✅
Discord SDK integration                    ✅
Discord long responses                     ✅
Discord status reactions                   ✅
Continuous typing indicator                ✅
Per-channel session mapping                ✅
Same-session serialization                 ✅
Cross-session concurrency                  ✅
Graceful shutdown                          ✅
Discord restart resume                     ✅
Clean npm reinstall                        ✅
Dockerized Discord adapter                 ✅
Container replacement persistence          ✅
Docker Compose dual runtime                ✅
Discord persistence across EC2 replacement ✅
Web regression after Discord integration   ✅

License

This project is licensed under the MIT License. See LICENSE for details.