Background
We need to provision one independent forum-cli account and one Single User API key for each of 8 VMs (devops, kali, elementary, studio, ailab, translate, debian, modiqi) on wpcommunity.com.
This sounded straightforward—but we hit several pitfalls along the way. Here’s a record of them.
Pitfall 1: MCP Discourse Tool Returns 404 for Some Users
Using Discourse MCP’s get_user command to query the 8 usernames, only modiqi returned successfully; all others yielded 404.
Yet when we checked via Rails runner with User.where(username: ...), we confirmed that 7 of the 8 users actually exist (only kali was missing).
Root Cause: These bot/system users likely have non-public profiles—or the MCP tool uses a public API endpoint instead of an admin one.
Lesson Learned: Don’t blindly trust MCP’s 404 responses. For bot or system users, always double-check using Rails runner or the admin API.
Pitfall 2: Single User API Keys Lack Admin Privileges
Wenpai’s API key is scoped to Single User, with admin: false. It cannot be used to create users or generate new API keys via the API.
Even attempting to use the username system with the same key fails outright with invalid_access.
Conclusion: Admin-level operations—such as creating users or managing API keys—must be performed via SSH → Docker → Rails runner. Given their infrequent nature, distributing a global admin API key isn’t justified.
Pitfall 3: The Escaping Nightmare of SSH + Docker + Rails Runner
Running Ruby code via ssh prod-b "docker exec app su discourse -c '...'" involves three layers of nested quoting—and escaping mistakes are easy to make.
Especially when mixing Ruby’s save!, string interpolation (#{}), and heredoc syntax, Bash may interpret ! as history expansion and escape it to \!, causing Ruby syntax errors.
Solution: Avoid inline Ruby entirely. Instead:
# Write the script locally
scp script.rb prod-b:/tmp/
ssh prod-b "docker cp /tmp/script.rb app:/tmp/ && \
docker exec app chown discourse:discourse /tmp/script.rb && \
docker exec app su discourse -c \
'cd /var/www/discourse && RAILS_DB=default bundle exec rails runner /tmp/script.rb'"
This is currently the most robust approach—fully sidestepping quoting and escaping issues.
Pitfall 4: User#approve No Longer Exists in Newer Discourse Versions
Calling user.approve(Discourse.system_user) after user creation raises undefined method 'approve'.
In newer Discourse versions (Rails 8 + Discourse 3.x), the approval logic has changed. Simply set the relevant attributes during creation:
user = User.create!(
username: "kali",
email: "[email protected]",
password: SecureRandom.hex(16),
active: true,
approved: true,
trust_level: 0
)
user.activate
Pitfall 5: ApiKey#key Is Only Readable Immediately After Creation
This is the sneakiest one. Discourse’s ApiKey model enforces strict access control on the key field:
API key is only accessible immediately after creation (ApiKey::KeyAccessError)
That means you cannot retrieve the plaintext key later—even via Rails—because only its hash is stored in the database.
Workaround: To obtain the plaintext key, first destroy all existing keys for that user, then create a new one and read key immediately:
ApiKey.where(user_id: user.id).destroy_all
api_key = ApiKey.new(user_id: user.id, description: "...", created_by_id: -1)
api_key.save!
raw_key = api_key.key # This is your *only* chance to read the plaintext key
Pitfall 6: Rails Runner Must Run as the discourse User
Running docker exec app rails runner ... as root triggers a PostgreSQL role error. You must switch to the discourse user:
docker exec app su discourse -c 'cd /var/www/discourse && bundle exec rails runner ...'
In multi-site environments, don’t forget to set RAILS_DB=default.
Final Outcome
| VM | User ID | Status |
|---|---|---|
| devops | 34 | Existing |
| kali | 44 | Newly created |
| elementary | 38 | Existing |
| studio | 39 | Existing |
| ailab | 40 | Existing |
| translate | 36 | Existing |
| debian | 43 | Existing |
| modiqi | 2 | Existing (admin) |
All 8 Single User API keys have been generated and saved to /mnt/shared-context/secrets/wpcommunity-vm-api-keys.json.