---
tags: smartcitizen, platform
---
# Smart Citizen Platform Deployment
[TOC]
## Requirements
* We need to have [docker](https://docs.docker.com/) installed.
* We need to clone the [`smartcitizen-api`]() repo
The platform is run in `docker` containers, and using `docker-compose` to manage them. Depending on the scenario, we will need to change the `docker-compose.yml` file for specifying what containers run.
:::warning
`docker-compose` has a separate development timeline to `docker`. Now, there is a `compose V2`, which is `docker compose`. In addition, the `docker-compose.yaml` file has different versions, that allow for different configuration options. We currently are with a version in production that has some featured and not others (supports swarm, but no memory limitations, for instance)
:::
Workflow (detailed in each case below):
1. Check the `.env` file for understanding what you need to deploy and where
2. Cassandra nodes local or elsewhere?
3. MQTT local or elsewhere?
4. URLs
:::info
If the infrastructure is deployed in the same provider (i.e. Linode) the IP addresses for each service can be local (i.e, MQTT is in a separate server but the IP address is a local one within linode). For this to work, in Linode, `Add an IP address` to the machine and Select `Privaet`.
:::
## Staging
### Environment
This would work for staging:
1. You need to assign some variables in the `.env` file:
```
# STAGING
DEFAULT_URL=staging-api.smartcitizen.me
RAILS_ENV=production
# This is the STAGING server but with its own db!
# It needs to be in production mode, not dev
# Production config/database.yml
DATABASE_URL=postgresql://postgres:postgres@db/sc_final
# Tell sidekiq to use hostname redis, not localhost
# If this var is set, Redis uses it by default. No config needed
REDIS_URL=redis://redis:6379/0/cache
REDIS_STORE=redis://redis:6379/3
#Minuteman uses this:
REDIC_URL=redis://redis:6379/1
# DISABLE if you don't want staging to send emails
#mailgun_api_key=x
#mailgun_public_api_key=x
#mailgun_domain=mailbot.smartcitizen.me
# Not needed (we have external emqx server)
# Debug: doco exec mqtt sh; tail -f log/*
#EMQX_LOG__LEVEL=error
# MQTT Staging (EMQX 5)
MQTT_HOST=192.168.134.195
MQTT_CLIENT_ID=smartcitizen-api-staging
# This is not needed below (we start one cassandra in )
# New cass cluster, dockerized cass05, cass06
#CASS_HOSTS=109.237.26.252:9160,178.79.189.22:9160
#CASS_HOSTS=109.237.26.252,178.79.189.22
#CASS_HOSTS=178.79.189.22
# Kairos is not enabled in dns for staging. access via ip:port
kairos_server=kairos
kairos_telnet_port=4242
kairos_port=8080
kairos_http_username=[...]
kairos_http_password=[...]
# AUTH id.smartcitizen.me
AUTH_SECRET_KEY_BASE=somerandomnumberjusttesting
#RAILS_SERVE_STATIC_FILES=true
RACK_ENV=production
PUMA_TOKEN=smartToken
# AWS secret
aws_access_key=xx
aws_secret_key=xx
aws_region=eu-west-1
s3_bucket=smartcitizen
# Comment below to avoid issues on sentry
# RAVEN_DSN_URL=https://x@x.ingest.sentry.io/5196011
```
2. In a terminal, run the command to start basic services (`app` and `db`)
```
doco up app db -d
```
This will start the app and the postgres.
3. If running first time, you need to run the following commands inside the app container to start the database:
```
docker-compose exec app bash
```
And then:
```
bin/rails db:create
```
```
bin/rails db:schema:load
```
```
bin/rails db:seed
```
4. Now you can start the other services:
```
doco up -d
```
5. Checking the output of `doco ps` you should see this:
```
root@staging-api:~/smartcitizen-api# doco ps
Name Command State Ports
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
smartcitizen-api_app_1 entrypoint.sh bin/rails se ... Up 0.0.0.0:3000->3000/tcp,:::3000->3000/tcp
smartcitizen-api_auth_1 rails server -p 3000 -b 0. ... Up 0.0.0.0:3001->3000/tcp,:::3001->3000/tcp
smartcitizen-api_cassandra-1_1 docker-entrypoint.sh cassa ... Up 0.0.0.0:7000->7000/tcp,:::7000->7000/tcp, 0.0.0.0:7001->7001/tcp,:::7001->7001/tcp,
0.0.0.0:7199->7199/tcp,:::7199->7199/tcp, 0.0.0.0:9042->9042/tcp,:::9042->9042/tcp,
0.0.0.0:9160->9160/tcp,:::9160->9160/tcp
smartcitizen-api_db_1 docker-entrypoint.sh postgres Up 5432/tcp
smartcitizen-api_kairos_1 /usr/bin/runkairos.sh run Up 2003/tcp, 2004/tcp, 0.0.0.0:4242->4242/tcp,:::4242->4242/tcp, 0.0.0.0:8080->8080/tcp,:::8080->8080/tcp
smartcitizen-api_mqtt-task_1 entrypoint.sh bundle exec ... Up 3000/tcp
smartcitizen-api_push_1 npm start Up 0.0.0.0:8000->8000/tcp,:::8000->8000/tcp
smartcitizen-api_redis_1 docker-entrypoint.sh redis ... Up 6379/tcp
smartcitizen-api_sidekiq_1 entrypoint.sh bundle exec ... Up 3000/tcp
smartcitizen-api_telnet-task_1 entrypoint.sh bundle exec ... Up 3000/tcp
smartcitizen-api_web_1 nginx -g daemon off; Up 0.0.0.0:80->80/tcp,:::80->80/tcp, 0.0.0.0:80->80/udp,:::80->80/udp
```
:::info
Note that this only deploys the api. It is all new and empty.
:::
### Adding a test device
1. Add an user:
```shell!
curl -X POST https://staging-api.smartcitizen.me/v0/users\?email\=eeee@fablabbcn.org\&username\=eeee\&password\=aaaaa
```
Get the token for that user:
```shell!
'https://staging-api.smartcitizen.me/v0/sessions' -X POST -H 'Content-Type: application/json;charset=UTF-8' --data-binary '{"username": "eeee", "password": "ueueueueue"}'
{"access_token":"AAAAAAAAAAAAAA"}%
```
2. Add a kit:
* Enter the `rails console`:
```shell!
doco exec app rails console
DEPRECATION WARNING: Including LoggerSilence is deprecated and will be removed in Rails 6.1. Please use `ActiveSupport::LoggerSilence` instead (called from <main> at /app/config/application.rb:18)
initialized a background worker with 2 threads
Loading production environment (Rails 6.0.3.3)
...
irb(main):001:0>
```
* Create Measurements
```shell
Measurement.create!(name: "air temperature", description: "Air temperature is a measure of how hot or cold the air is. It is the most commonly measured weather parameter. Air temperature is dependent on the amount and strength of the sunlight hitting the earth, and atmospheric conditions, such as cloud cover and humidity, which trap heat.")
Measurement.create!(name: "relative humidity", description: "Relative humidity is a measure of the amount of moisture in the air relative to the total amount of moisture the air can hold. For instance, if the relative humidity was 50%, then the air is only half saturated with moisture.")
Measurement.create!(name: "light", description: "Lux is a measure of how much light is spread over a given area. A full moon clear night is around 1 lux, inside an office building you usually have 400 lux and a bright day can be more than 20000 lux.")
Measurement.create!(name: "noise", description: "dB's measure sound pressure difference between the average local pressure and the pressure in the sound wave. A quiet library is below 40dB, your house is around 50dB and a diesel truck in your street 90dB.")
Measurement.create!(name: "PM 1", description: "PM stands for particulate matter: the term for a mixture of solid particles and liquid droplets found in the air. Some particles, such as dust, dirt, soot, or smoke, are large or dark enough to be seen with the naked eye.")
Measurement.create!(name: "PM 2.5", description: "PM stands for particulate matter: the term for a mixture of solid particles and liquid droplets found in the air. Some particles, such as dust, dirt, soot, or smoke, are large or dark enough to be seen with the naked eye.")
Measurement.create!(name: "PM 10", description: "PM stands for particulate matter: the term for a mixture of solid particles and liquid droplets found in the air. Some particles, such as dust, dirt, soot, or smoke, are large or dark enough to be seen with the naked eye.")
Measurement.create!(name: "eCO2", description: "Equivalent CO2 is the concentration of CO2 that would cause the same level of radiative forcing as a given type and concentration of greenhouse gas. Examples of such greenhouse gases are methane, perfluorocarbons, and nitrous oxide. CO2 is primarily a by-product of human metabolism and is constantly being emitted into the indoor environment by building occupants. CO2 may come from combustion sources as well. Associations of higher indoor carbon dioxide concentrations with impaired work performance and increased health symptoms have been attributed to correlation of indoor CO2 with concentrations of other indoor air pollutants that are also influenced by rates of outdoor-air ventilation.")
Measurement.create!(name: "tVOC", description: "Total volatile organic compounds is a grouping of a wide range of organic chemical compounds to simplify reporting when these are present in ambient air or emissions. Many substances, such as natural gas, could be classified as volatile organic compounds (VOCs).")
Measurement.create!(name: "battery", description: "The SCK remaining battery level in percentage.")
Measurement.create!(name: "Barometric Pressure", description: "Barometric pressure is the pressure within the atmosphere of Earth. In most circumstances atmospheric pressure is closely approximated by the hydrostatic pressure caused by the weight of air above the measurement point.")
```
* Create Sensors (same as current kit):
```
Sensor.create!(id: 10, name: "Battery SCK", description: "Custom Circuit", unit: "%", measurement_id: 10)
Sensor.create!(id: 14, name: "BH1730FVC - Light", description: "Digital Ambient Light Sensor", unit: "lux", measurement_id: 3)
Sensor.create!(id: 53, name: "ICS43432 - Noise", description: "I2S Digital Mems Microphone with custom Audio Proc...", unit: "dBA", measurement_id: 4)
Sensor.create!(id: 55, name: "SHT31 - Temperature", description: "Temperature", unit: "ºC", measurement_id: 1)
Sensor.create!(id: 56, name: "SHT31 - Humidity", description: "Humidity", unit: "%", measurement_id: 2)
Sensor.create!(id: 58, name: "MPL3115A2 - Barometric Pressure", description: "Digital Barometric Pressure Sensor", unit: "kPa", measurement_id: 11)
Sensor.create!(id: 87, name: "PMS5003 - PM2.5", description: "Particle Matter PM 2.5", unit: "ug/m3", measurement_id: 6)
Sensor.create!(id: 88, name: "PMS5003 - PM10", description: "Particle Matter PM 10", unit: "ug/m3", measurement_id: 7)
Sensor.create!(id: 89, name: "PMS5003 - PM1.0", description: "Particle Matter PM 1", unit: "ug/m3", measurement_id: 5)
Sensor.create!(id: 112, name: "AMS CCS811 - eCO2", description: "Equivalent Carbon Dioxide Digital Indoor Sensor", unit: "ppm", measurement_id: 8)
Sensor.create!(id: 113, name: "AMS CCS811 - TVOC", description: "Total Volatile Organic Compounds Digital Indoor Se...", unit: "ppb", measurement_id: 9)
```
* Create Kit
```
Kit.create!(name: "SCK 2.1", description: "Smart Citizen Kit 2.1 with Urban Sensor Board", id: 26, slug: "sck:2,1")
```
* Create components
```
Component.create!(id:1, board_id:26, board_type: "Kit", sensor_id: 10)
Component.create!(id:2, board_id:26, board_type: "Kit", sensor_id: 14)
Component.create!(id:3, board_id:26, board_type: "Kit", sensor_id: 53)
Component.create!(id:4, board_id:26, board_type: "Kit", sensor_id: 55)
Component.create!(id:5, board_id:26, board_type: "Kit", sensor_id: 56)
Component.create!(id:6, board_id:26, board_type: "Kit", sensor_id: 58)
Component.create!(id:7, board_id:26, board_type: "Kit", sensor_id: 87)
Component.create!(id:8, board_id:26, board_type: "Kit", sensor_id: 88)
Component.create!(id:9, board_id:26, board_type: "Kit", sensor_id: 89)
Component.create!(id:10, board_id:26, board_type: "Kit", sensor_id: 112)
Component.create!(id:11, board_id:26, board_type: "Kit", sensor_id: 113)
```
* Create Device
```
Device.create!(owner_id:1, name: "Test", kit_id: 26, device_token: "7cfa45", latitude: 41.458030, longitude: 2.211520)
...
=> #<Device id: 1, owner_id: 1, name: "Test", description: nil, mac_address: nil, latitude: 41.45803, longitude: 2.21152, created_at: "2022-12-12 11:56:35", updated_at: "2022-12-12 11:56:35", kit_id: 26, latest_data: nil, geohash: "sp3ef74qs0", last_recorded_at: nil, meta: nil, location: {"address"=>"Institut Numància, Carrer de Prat de la Riba, Santa Coloma de Gramenet, Barcelonès, Barcelona, Catalonia, 08921, Spain", "city"=>"Santa Coloma de Gramenet", "postal_code"=>"08921", "state_name"=>"Catalonia", "state_code"=>"Catalonia", "country_code"=>"es"}, data: nil, old_data: nil, owner_username: nil, uuid: nil, migration_data: nil, workflow_state: "active", csv_export_requested_at: nil, old_mac_address: nil, state: "never_published", device_token: "7cfa45", hardware_info: nil, notify_stopped_publishing_timestamp: "2019-01-16 16:19:35", notify_low_battery_timestamp: "2019-01-16 16:19:35", notify_low_battery: false, notify_stopped_publishing: false, is_private: false, is_test: false>
```
:::warning
Doing `doco start -d` doesn't start kairos properly...
`doco ps`
```
root@staging-api:~/smartcitizen-api# doco ps
Name Command State Ports
---------------------------------------------------------------------------------------
smartcitizen-api_app_1 entrypoint.sh bin/rails Up 0.0.0.0:3000->3000/tcp
se ... ,:::3000->3000/tcp
smartcitizen-api_auth_1 rails server -p 3000 -b Up 0.0.0.0:3001->3000/tcp
0. ... ,:::3001->3000/tcp
smartcitizen- docker-entrypoint.sh Up 0.0.0.0:7000->7000/tcp
api_cassandra-1_1 cassa ... ,:::7000->7000/tcp, 0.
0.0.0:7001->7001/tcp,:
::7001->7001/tcp, 0.0.
0.0:7199->7199/tcp,:::
7199->7199/tcp, 0.0.0.
0:9042->9042/tcp,:::90
42->9042/tcp, 0.0.0.0:
9160->9160/tcp,:::9160
->9160/tcp
smartcitizen-api_db_1 docker-entrypoint.sh Up 5432/tcp
postgres
smartcitizen- /usr/bin/runkairos.sh Exit 0
api_kairos_1 run
smartcitizen-api_mqtt- entrypoint.sh bundle Up 3000/tcp
task_1 exec ...
smartcitizen-api_push_1 npm start Up 0.0.0.0:8000->8000/tcp
,:::8000->8000/tcp
smartcitizen- docker-entrypoint.sh Up 6379/tcp
api_redis_1 redis ...
smartcitizen- entrypoint.sh bundle Up 3000/tcp
api_sidekiq_1 exec ...
smartcitizen- entrypoint.sh bundle Restarting
api_telnet-task_1 exec ...
smartcitizen-api_web_1 nginx -g daemon off; Up 0.0.0.0:80->80/tcp,:::
80->80/tcp, 0.0.0.0:80
->80/udp,:::80->80/udp
root@staging-api:~/smartcitizen-api# doco start kairos
Starting kairos ... done
```
:::
### Test basic requests
* Get historical readings from device
```
https://staging-api.smartcitizen.me/v0/devices/1/readings?sensor_id=55&rollup=1m
```
## Resources limitations
:::warning
Currently we can't put in _resources limitations_ because do not use the right compose file format.
We need to update docker-compose (standalone). **Be careful** now there is a change and docker also has compose in it in the new releases ([Compose V2](https://docs.docker.com/compose/reference/))
:::
:::danger
This might not work in production so **easily** because we have a more complex system!
:::
### Upgrade `docker-compose`
1. Rename `docker-compose` just in case
```
$ mv /usr/bin/docker-compose /usr/bin/docker-compose.bak
```
2. Get new `docker-compose` standalone: https://docs.docker.com/compose/install/other/
3. Add symbolic link
```
$ ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose
```
4. Test:
```
$ docker-compose --version
Docker Compose version v2.14.0
```
### Update `docker-compose.yml` file
:::info
No need to specify anymore the compose file version at the top
:::
Updating **only** the `app` container:
```
app:
build:
context: .
# Skip installing development & test gems in production, saves 20s build time.
# If developing with Docker, this line might need to be commented out.
args:
- BUNDLE_WITHOUT=test development
env_file: .env
ports:
- "3000:3000"
depends_on:
# We disable some containers in production
- db
- auth
- redis
- sidekiq
- mqtt-task
- telnet-task
- push
#- mqtt
restart: always
deploy:
resources:
limits:
memory: 2gb
volumes:
- "./:/app"
#command: rails server -p 3000 -b '0.0.0.0'
#command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
environment:
# For this to take effect, remove init/timeout, and in Gemfile, fix "rack-timeout" to not require anything.
RACK_TIMEOUT_SERVICE_TIMEOUT: 25
logging:
driver: "json-file"
options:
max-size: "100m"
```
And running:
```
docker-compose up -d
```
Now we should see limitations in the memory usage of the container:
```
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
012cebc5c96e smartcitizen-api-web-1 0.00% 2.625MiB / 3.84GiB 0.07% 1.47kB / 0B 365kB / 0B 2
1f69e8ce224d smartcitizen-api-app-1 0.04% 124.5MiB / **2GiB** 6.08% 1.58kB / 0B 9.91MB / 4.1kB 14
9fac3eb24614 smartcitizen-api-kairos-1 0.15% 162.4MiB / 3.84GiB 4.13% 48.1kB / 29.7kB 12.5MB / 77.8kB 45
28aedc5aafce smartcitizen-api-telnet-task-1 0.00% 0B / 0B 0.00% 0B / 0B 0B / 0B 0
c58670cc61ad smartcitizen-api-redis-1 0.15% 2.426MiB / 3.84GiB 0.06% 604kB / 521kB 3.95MB / 0B 4
83ffce7c1113 smartcitizen-api-db-1 0.03% 24.74MiB / 3.84GiB 0.63% 135kB / 605kB 2.77MB / 963kB 8
53df592cea94 smartcitizen-api-cassandra-1-1 0.50% 1.184GiB / 3.84GiB 30.84% 31.1kB / 47.6kB 6.22MB / 3.08MB 58
b507b1bba92e smartcitizen-api-push-1 0.00% 43.02MiB / 3.84GiB 1.09% 2.41MB / 12.4kB 258kB / 16.4kB 23
83a83f57caea smartcitizen-api-sidekiq-1 0.17% 182.2MiB / 3.84GiB 4.63% 162kB / 192kB 3.96MB / 6.89MB 31
b7280385023c smartcitizen-api-auth-1 0.00% 61.2MiB / 3.84GiB 1.56% 1.8kB / 0B 2MB / 0B 6
097bc0751948 smartcitizen-api-mqtt-task-1 0.06% 159.1MiB / 3.84GiB 4.05% 637kB / 545kB 4.72MB / 6.89MB 4
```
:::warning
We may want to disable `swap` usage in production to avoid problems? This is done in `mqtt-staging` as per recommendation of `emqx` docs, and in `api-staging` for testing purposes...
:::
## Security